#!/usr/bin/env node 'use strict' const assert = require('node:assert/strict') const crypto = require('node:crypto') const fs = require('node:fs') const fsp = require('node:fs/promises') const http = require('node:http') const net = require('node:net') const os = require('node:os') const path = require('node:path') const { execFileSync, spawn } = require('node:child_process') const ROOT = path.resolve(__dirname, '..') const MANIFEST_URL = 'http://192.168.10.6/ycode/releases/latest.json' const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/ function readExpectedVersion() { const args = process.argv.slice(2) let value = null for (let index = 0; index < args.length; index += 1) { const argument = args[index] if (argument === '--expected-version') { value = args[++index] } else if (argument.startsWith('--expected-version=')) { value = argument.slice('--expected-version='.length) } else { throw new Error(`Unknown argument: ${argument}`) } } if (value === null) value = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version if (typeof value !== 'string' || !SEMVER_RE.test(value)) throw new Error(`Invalid expected version: ${value}`) return value } const EXPECTED_VERSION = readExpectedVersion() const PACKAGED_EXE = path.resolve(process.env.YCODE_QA_PACKAGED_EXE || path.join(ROOT, 'release', 'win-unpacked', 'YCode.exe')) const PUBLIC_KEY = path.join(ROOT, 'resources', 'updates', 'public-key.pem') const OUTPUT_ROOT = path.join(ROOT, '.tmp', 'updates-live') const STAMP = new Date().toISOString().replace(/[:.]/g, '-') const RUN_ROOT = path.join(OUTPUT_ROOT, STAMP) const REPORT_PATH = path.join(RUN_ROOT, 'report.json') const LATEST_REPORT_PATH = path.join(OUTPUT_ROOT, 'latest-report.json') const SCREENSHOT_PATH = path.join(RUN_ROOT, 'production-boot.png') const REGISTRY_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\8a4ee941-d893-569b-8bcb-2a55d270c04b' const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)) let activeBootChild = null async function handleInterrupt(signal) { await stopExactChild(activeBootChild) process.exit(signal === 'SIGINT' ? 130 : 143) } process.once('SIGINT', () => void handleInterrupt('SIGINT')) process.once('SIGTERM', () => void handleInterrupt('SIGTERM')) function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex') } async function sha256File(filename) { const hash = crypto.createHash('sha256') for await (const chunk of fs.createReadStream(filename)) hash.update(chunk) return hash.digest('hex') } async function writeJsonAtomic(filename, value) { await fsp.mkdir(path.dirname(filename), { recursive: true }) const temporary = `${filename}.${process.pid}.tmp` await fsp.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8') await fsp.rename(temporary, filename) } function errorDetails(error) { if (error instanceof Error) return { name: error.name, message: error.message, stack: error.stack } return { name: 'Error', message: String(error) } } async function fetchBytes(url, maximum = 1024 * 1024, timeoutMs = 30_000) { let current = new URL(url) const origin = current.origin for (let redirects = 0; redirects <= 5; redirects += 1) { const response = await fetch(current, { redirect: 'manual', cache: 'no-store', signal: AbortSignal.timeout(timeoutMs) }) if ([301, 302, 303, 307, 308].includes(response.status)) { const location = response.headers.get('location') await response.body?.cancel().catch(() => undefined) if (!location || redirects === 5) throw new Error('Manifest redirect chain is invalid') const next = new URL(location, current) if (next.origin !== origin) throw new Error('Manifest redirect left the configured origin') current = next continue } if (!response.ok) throw new Error(`Manifest request failed with HTTP ${response.status}`) const declared = Number(response.headers.get('content-length') || 0) if (declared > maximum) throw new Error('Manifest response exceeds the size limit') const bytes = Buffer.from(await response.arrayBuffer()) if (bytes.length > maximum) throw new Error('Manifest response exceeds the size limit') return bytes } throw new Error('Manifest redirect chain is invalid') } async function reserveLoopbackPort() { const server = net.createServer() await new Promise((resolve, reject) => { server.once('error', reject) server.listen(0, '127.0.0.1', resolve) }) const address = server.address() await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) if (!address || typeof address === 'string') throw new Error('Unable to reserve a debug port') return address.port } function getJson(url, timeoutMs = 2_000) { return new Promise((resolve, reject) => { const request = http.get(url, { timeout: timeoutMs, family: 4 }, response => { const chunks = [] response.on('data', chunk => chunks.push(chunk)) response.on('end', () => { if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode} from ${url}`)) return } try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))) } catch (error) { reject(error) } }) }) request.on('timeout', () => request.destroy(new Error('CDP endpoint timed out'))) request.on('error', reject) }) } async function waitForCdpPage(port, child) { const deadline = Date.now() + 45_000 while (Date.now() < deadline) { if (child.exitCode !== null) throw new Error(`Packaged app exited early with code ${child.exitCode}`) try { const targets = await getJson(`http://127.0.0.1:${port}/json/list`) const page = targets.find(target => target.type === 'page' && typeof target.webSocketDebuggerUrl === 'string' && (/\/index\.html(?:[?#]|$)/.test(target.url) || /\/$/.test(target.url))) if (page) return page } catch { // Chromium's endpoint is not ready yet. } await sleep(250) } throw new Error('Timed out waiting for the packaged app renderer target') } function openWebSocket(url) { const WebSocketCtor = globalThis.WebSocket || require('ws') return new Promise((resolve, reject) => { const socket = new WebSocketCtor(url) const timer = setTimeout(() => reject(new Error('CDP WebSocket open timed out')), 15_000) const cleanup = () => { clearTimeout(timer) socket.removeEventListener?.('open', onOpen) socket.removeEventListener?.('error', onError) } const onOpen = () => { cleanup() resolve(socket) } const onError = error => { cleanup() reject(error instanceof Error ? error : new Error('CDP WebSocket failed')) } socket.addEventListener('open', onOpen) socket.addEventListener('error', onError) }) } class CdpClient { constructor(socket) { this.socket = socket this.nextId = 1 this.pending = new Map() socket.addEventListener('message', event => { let message try { const text = typeof event.data === 'string' ? event.data : Buffer.from(event.data).toString('utf8') message = JSON.parse(text) } catch { return } if (!message.id) return const pending = this.pending.get(message.id) if (!pending) return this.pending.delete(message.id) clearTimeout(pending.timer) if (message.error) pending.reject(new Error(`${message.error.message || 'CDP error'} (${message.error.code})`)) else pending.resolve(message.result) }) socket.addEventListener('close', () => { for (const pending of this.pending.values()) { clearTimeout(pending.timer) pending.reject(new Error('CDP WebSocket closed')) } this.pending.clear() }) } static async connect(url) { return new CdpClient(await openWebSocket(url)) } request(method, params = {}, timeoutMs = 30_000) { const id = this.nextId this.nextId += 1 return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id) reject(new Error(`CDP request timed out: ${method}`)) }, timeoutMs) this.pending.set(id, { resolve, reject, timer }) this.socket.send(JSON.stringify({ id, method, params })) }) } async evaluate(expression) { const result = await this.request('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true, userGesture: false }) if (result.exceptionDetails) { const description = result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Renderer evaluation failed' throw new Error(description) } return result.result?.value } close() { try { this.socket.close() } catch { // The renderer may already be gone. } } } async function waitForUpdatesApi(client, child) { const deadline = Date.now() + 30_000 while (Date.now() < deadline) { if (child.exitCode !== null) throw new Error(`Packaged app exited early with code ${child.exitCode}`) try { const ready = await client.evaluate( "typeof window.ycode?.updates?.getState === 'function' && document.readyState === 'complete'" ) if (ready === true) return } catch { // React/preload may still be starting. } await sleep(250) } throw new Error('Timed out waiting for window.ycode.updates') } const UPDATER_INSPECTION_EXPRESSION = `(async () => { const api = window.ycode?.updates if (!api) throw new Error('window.ycode.updates is unavailable') const apiMethods = ['getState', 'check', 'download', 'install', 'dismiss', 'openDownloadPage', 'openLog', 'onState'] .filter(name => typeof api[name] === 'function') const before = await api.getState() const events = [] const unsubscribe = api.onState(state => events.push(state)) let after try { after = await api.check() const deadline = Date.now() + 15000 while (Date.now() < deadline) { if (events.some(state => state.status === 'up-to-date')) break if (events.some(state => state.status === 'error' || state.status === 'unsupported')) break await new Promise(resolve => setTimeout(resolve, 100)) } await new Promise(resolve => setTimeout(resolve, 500)) } finally { unsubscribe() } return { apiMethods, before, after, events } })()` async function captureScreenshot(client, filename) { await client.request('Page.enable') const screenshot = await client.request('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false }) await fsp.writeFile(filename, Buffer.from(screenshot.data, 'base64')) } async function stopExactChild(child) { if (!child || child.exitCode !== null || child.signalCode !== null) return const exited = new Promise(resolve => child.once('exit', resolve)) child.kill() await Promise.race([exited, sleep(5_000)]) if (child.exitCode === null && child.signalCode === null) { child.kill('SIGKILL') await Promise.race([exited, sleep(2_000)]) } } async function runProductionBoot(expectedManifest) { if (!fs.existsSync(PACKAGED_EXE)) throw new Error(`Packaged executable is missing: ${PACKAGED_EXE}`) const port = await reserveLoopbackPort() const userData = path.join(RUN_ROOT, 'production-boot-user-data') await fsp.mkdir(userData, { recursive: true }) const stdoutPath = path.join(RUN_ROOT, 'production-boot.stdout.log') const stderrPath = path.join(RUN_ROOT, 'production-boot.stderr.log') const stdout = fs.createWriteStream(stdoutPath, { flags: 'w' }) const stderr = fs.createWriteStream(stderrPath, { flags: 'w' }) const env = { ...process.env, YCODE_USER_DATA_DIR: userData, YCODE_SKIP_KERNEL_DATA_RESET: '1', YCODE_QA_OFFSCREEN: '1' } delete env.ELECTRON_RENDERER_URL delete env.YCODE_QA_DIRECTORY for (const key of ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'YCODE_API_KEY', 'CODEBUDDY_API_KEY']) delete env[key] const child = spawn(PACKAGED_EXE, [ `--remote-debugging-port=${port}`, '--remote-debugging-address=127.0.0.1' ], { cwd: RUN_ROOT, env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) child.stdout.pipe(stdout) child.stderr.pipe(stderr) activeBootChild = child let client let inspection let screenshotWritten = false try { if (!child.pid) throw new Error('Packaged app did not expose a child PID') const page = await waitForCdpPage(port, child) client = await CdpClient.connect(page.webSocketDebuggerUrl) await waitForUpdatesApi(client, child) inspection = await client.evaluate(UPDATER_INSPECTION_EXPRESSION) await captureScreenshot(client, SCREENSHOT_PATH) screenshotWritten = true assert.equal(inspection.before.currentVersion, EXPECTED_VERSION) assert.equal(inspection.after.currentVersion, EXPECTED_VERSION) assert.equal(inspection.after.status, 'up-to-date') assert.equal(inspection.after.lastExplicit, true) assert.equal(inspection.apiMethods.length, 8) assert.equal(inspection.events.some(state => state.status === 'checking'), true) assert.equal(inspection.events.some(state => state.status === 'up-to-date'), true) assert.equal(inspection.events.some(state => ['downloading', 'ready', 'installing'].includes(state.status)), false) assert.equal(inspection.events.every(state => state.currentVersion === EXPECTED_VERSION), true) return { mode: 'actual-production-packaged-boot', executable: PACKAGED_EXE, childPid: child.pid, debugPort: port, isolatedUserData: userData, expectedServerVersion: expectedManifest.version, apiMethods: inspection.apiMethods, before: inspection.before, after: inspection.after, sharedIpcEvents: inspection.events, screenshot: screenshotWritten ? SCREENSHOT_PATH : null, stdoutLog: stdoutPath, stderrLog: stderrPath, installerOrUpgradeRequested: false, passed: true } } finally { client?.close() await stopExactChild(child) if (activeBootChild === child) activeBootChild = null stdout.end() stderr.end() } } function createRuntimeEntry(filename) { const relative = value => `./${path.relative(path.dirname(filename), value).replace(/\\/g, '/')}` return [ `export { UpdateManager } from ${JSON.stringify(relative(path.join(ROOT, 'src/main/updates/manager.ts')))}`, `export { FileUpdatePersistence } from ${JSON.stringify(relative(path.join(ROOT, 'src/main/updates/storage.ts')))}`, `export { verifySignedManifest } from ${JSON.stringify(relative(path.join(ROOT, 'src/main/updates/manifest.ts')))}`, '' ].join('\n') } function bundleActualManager(runtimeDirectory) { fs.mkdirSync(runtimeDirectory, { recursive: true }) const entry = path.join(runtimeDirectory, 'entry.ts') const output = path.join(runtimeDirectory, 'updater-runtime.cjs') fs.writeFileSync(entry, createRuntimeEntry(entry), 'utf8') const esbuild = path.join(ROOT, 'node_modules', 'esbuild', 'bin', 'esbuild') if (!fs.existsSync(esbuild)) throw new Error(`esbuild is missing: ${esbuild}`) execFileSync(process.execPath, [ esbuild, entry, '--bundle', '--platform=node', '--format=cjs', '--target=node22', `--outfile=${output}`, '--log-level=warning' ], { cwd: ROOT, stdio: 'inherit' }) return { entry, output, runtime: require(output) } } function forbiddenInstaller() { const calls = { validateEnvironment: 0, verifyInstaller: 0, launch: 0 } const fail = name => { calls[name] += 1 throw new Error('Live verification is download-only; installer access is forbidden') } return { calls, adapter: { validateEnvironment: async () => fail('validateEnvironment'), verifyInstaller: async () => fail('verifyInstaller'), launch: async () => fail('launch') } } } async function runSimulatedPredecessorDownload(expectedManifest, runtime, publicKeyPem) { const directory = path.join(RUN_ROOT, 'simulated-predecessor') const cacheDirectory = path.join(directory, 'cache') const persistence = new runtime.FileUpdatePersistence(path.join(directory, 'state.json')) const forbidden = forbiddenInstaller() const callbacks = { beforeInstall: 0, quitForUpdate: 0, installFailed: 0 } const events = [] const manager = new runtime.UpdateManager({ currentVersion: '0.0.1', manifestUrl: MANIFEST_URL, cacheDirectory, logFile: path.join(directory, 'updater.log'), persistence, publicKey: async () => publicKeyPem, supported: true, installer: forbidden.adapter, beforeInstall: async () => { callbacks.beforeInstall += 1 throw new Error('beforeInstall is forbidden in live download verification') }, quitForUpdate: async () => { callbacks.quitForUpdate += 1 throw new Error('quitForUpdate is forbidden in live download verification') }, installFailed: () => { callbacks.installFailed += 1 }, autoDownload: false, manifestTimeoutMs: 60_000, downloadTimeoutMs: 20 * 60_000 }) let lastLoggedProgress = -1 const unsubscribe = manager.subscribe(state => { events.push(state) if (state.status === 'downloading' && typeof state.progress === 'number') { const bucket = Math.floor(state.progress / 10) * 10 if (bucket > lastLoggedProgress) { lastLoggedProgress = bucket console.log(`simulated predecessor download: ${state.progress}%`) } } }) try { const checked = await manager.check(true) assert.equal(checked.status, 'available') assert.equal(checked.version, expectedManifest.version) const downloaded = await manager.download(true) assert.equal(downloaded.status, 'ready') assert.equal(downloaded.progress, 100) assert.equal(downloaded.currentVersion, '0.0.1') assert.equal(downloaded.version, expectedManifest.version) assert.equal(downloaded.lastExplicit, true) const filename = `YCode-${expectedManifest.version}-Setup.exe` const cachedPath = path.join(cacheDirectory, filename) const info = await fsp.stat(cachedPath) const digest = await sha256File(cachedPath) assert.equal(info.isFile(), true) assert.equal(info.size, expectedManifest.size) assert.equal(digest, expectedManifest.sha256) assert.equal(path.basename(cachedPath), filename) const persisted = await persistence.load() assert.equal(persisted.unfinishedVersion, undefined) assert.equal(persisted.target, undefined) assert.deepEqual(forbidden.calls, { validateEnvironment: 0, verifyInstaller: 0, launch: 0 }) assert.deepEqual(callbacks, { beforeInstall: 0, quitForUpdate: 0, installFailed: 0 }) assert.equal(events.some(state => state.status === 'downloading'), true) return { mode: 'simulated-predecessor-real-network-download', simulatedPredecessorVersion: '0.0.1', actualApplicationVersionChanged: false, manifestUrl: MANIFEST_URL, expectedVersion: expectedManifest.version, state: downloaded, observedDownloading: true, cachedPath, size: info.size, sha256: digest, installerAdapterCalls: forbidden.calls, installCallbacks: callbacks, passed: true } } finally { unsubscribe() manager.dispose() } } function readRealUserState() { let registryFingerprint = null let registryEntryPresent = false try { const output = execFileSync('reg.exe', ['query', REGISTRY_KEY], { encoding: 'utf8', windowsHide: true, timeout: 10_000, maxBuffer: 64 * 1024 }) registryFingerprint = sha256(output) registryEntryPresent = true } catch { // The current user may not have a production installation. } let installedExeFingerprint = null let installedExePresent = false const installedExe = path.join(process.env.LOCALAPPDATA || '', 'Programs', 'YCode', 'YCode.exe') try { const info = fs.statSync(installedExe) installedExePresent = true installedExeFingerprint = sha256(`${installedExe}|${info.size}|${info.mtimeMs}`) } catch { // Optional read-only check only. } return { registryEntryPresent, registryFingerprint, installedExePresent, installedExeFingerprint } } async function main() { await fsp.mkdir(RUN_ROOT, { recursive: true }) const realUserStateBefore = readRealUserState() const report = { schemaVersion: 1, startedAt: new Date().toISOString(), finishedAt: null, manifestUrl: MANIFEST_URL, expectedVersion: EXPECTED_VERSION, realApplicationUpgradePerformed: false, installerLaunched: false, checks: { productionPackagedBoot: null, simulatedPredecessorDownload: null }, realUserStateBefore, realUserStateAfter: null, limitations: [ 'Production boot verifies only packaged updater IPC and up-to-date state.', 'Installer download uses a simulated predecessor version 0.0.1 manager.', 'No real application upgrade, installer launch, deployment, or replacement is performed.' ] } let exitCode = 0 try { const publicKeyPem = await fsp.readFile(PUBLIC_KEY, 'utf8') const runtime = bundleActualManager(path.join(RUN_ROOT, 'runtime')).runtime const manifestBytes = await fetchBytes(MANIFEST_URL) const manifest = runtime.verifySignedManifest(manifestBytes, { manifestUrl: MANIFEST_URL, publicKeyPem }) assert.equal(manifest.payload.version, EXPECTED_VERSION) const platform = manifest.payload.platforms['windows-x64'] const expectedManifest = { version: manifest.payload.version, url: platform.url, size: platform.size, sha256: platform.sha256 } report.verifiedManifest = expectedManifest try { report.checks.productionPackagedBoot = await runProductionBoot(expectedManifest) } catch (error) { exitCode = 1 report.checks.productionPackagedBoot = { mode: 'actual-production-packaged-boot', passed: false, error: errorDetails(error) } } try { report.checks.simulatedPredecessorDownload = await runSimulatedPredecessorDownload( expectedManifest, runtime, publicKeyPem ) } catch (error) { exitCode = 1 report.checks.simulatedPredecessorDownload = { mode: 'simulated-predecessor-real-network-download', simulatedPredecessorVersion: '0.0.1', actualApplicationVersionChanged: false, passed: false, error: errorDetails(error) } } } catch (error) { exitCode = 1 report.preflightError = errorDetails(error) } finally { report.realUserStateAfter = readRealUserState() report.realUserStateUnchanged = JSON.stringify(report.realUserStateBefore) === JSON.stringify(report.realUserStateAfter) report.finishedAt = new Date().toISOString() report.passed = exitCode === 0 await writeJsonAtomic(REPORT_PATH, report) await writeJsonAtomic(LATEST_REPORT_PATH, report) console.log(`report: ${REPORT_PATH}`) if (fs.existsSync(SCREENSHOT_PATH)) console.log(`screenshot: ${SCREENSHOT_PATH}`) } process.exitCode = exitCode } main().catch(async error => { console.error(error) process.exitCode = 1 try { await fsp.mkdir(RUN_ROOT, { recursive: true }) await writeJsonAtomic(REPORT_PATH, { schemaVersion: 1, startedAt: null, finishedAt: new Date().toISOString(), manifestUrl: MANIFEST_URL, expectedVersion: EXPECTED_VERSION, fatalError: errorDetails(error), passed: false, realApplicationUpgradePerformed: false, installerLaunched: false }) } catch { // Preserve the original failure. } })