const assert = require('node:assert/strict'); const { EventEmitter, once } = require('node:events'); const fs = require('node:fs'); const http = require('node:http'); const os = require('node:os'); const path = require('node:path'); const { PassThrough } = require('node:stream'); const test = require('node:test'); const { SidecarManager, buildChildEnv, bundledSidecarPath, probeHealth, reserveLoopbackPort, waitForHealth, } = require('../sidecar.cjs'); function fixture() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-sidecar-')); const resourcesPath = path.join(root, 'resources'); const apsHome = path.join(root, '.aps'); fs.mkdirSync(path.join(resourcesPath, 'sidecar'), { recursive: true }); fs.mkdirSync(path.join(resourcesPath, 'web', 'dist'), { recursive: true }); fs.writeFileSync(path.join(resourcesPath, 'sidecar', 'aps-sidecar.exe'), 'fixture'); fs.writeFileSync(path.join(resourcesPath, 'web', 'dist', 'index.html'), ''); return { root, resourcesPath, apsHome }; } function fakeChild(pid) { const child = new EventEmitter(); child.pid = pid; child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.killed = false; child.kill = () => { child.killed = true; child.emit('exit', 0, null); return true; }; return child; } test('reserves a loopback port', async () => { const port = await reserveLoopbackPort(); assert.ok(Number.isInteger(port)); assert.ok(port > 0 && port < 65_536); }); test('requires the packaged sidecar executable', () => { const { root, resourcesPath } = fixture(); try { assert.equal( bundledSidecarPath(resourcesPath, 'win32'), path.join(resourcesPath, 'sidecar', 'aps-sidecar.exe'), ); assert.throws(() => bundledSidecarPath(path.join(root, 'missing'), 'win32'), /missing/); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('health wait retries transient failures', async () => { let attempts = 0; await waitForHealth('http://127.0.0.1:1/api/health', { timeoutMs: 500, intervalMs: 1, probe: async () => { attempts += 1; if (attempts < 3) throw new Error('not ready'); }, }); assert.equal(attempts, 3); }); test('health probe requires the expected sidecar nonce', async () => { const nonce = 'c'.repeat(64); const received = []; const server = http.createServer((request, response) => { received.push(request.headers['x-aps-sidecar-nonce']); response.writeHead(200, { 'X-APS-Sidecar-Nonce': nonce }); response.end('{"ok":true}'); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); const url = `http://127.0.0.1:${address.port}/api/health`; try { await probeHealth(url, { expectedNonce: nonce }); await assert.rejects( probeHealth(url, { expectedNonce: 'd'.repeat(64) }), /unexpected sidecar identity/, ); assert.deepEqual(received, [nonce, 'd'.repeat(64)]); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test('cleans Conda and Python environment state before launching a frozen sidecar', () => { const condaRoot = 'D:\\AppData\\anaconda3'; const env = buildChildEnv( { PATH: [ condaRoot, `${condaRoot}\\Library\\bin`, `${condaRoot}\\Library\\mingw-w64\\bin`, `${condaRoot}\\Scripts`, `${condaRoot}\\condabin`, 'C:\\Windows\\System32', 'C:\\Program Files\\Git\\cmd', 'C:\\APS\\Scripts', ].join(';'), CONDA_PREFIX: condaRoot, CONDA_DEFAULT_ENV: 'base', CONDA_EXE: `${condaRoot}\\Scripts\\conda.exe`, PYTHONHOME: 'bad-home', PythonPath: 'bad-path', PYTHONUSERBASE: 'bad-user-site', WEB_CONCURRENCY: '2', UVICORN_WORKERS: '3', GUNICORN_CMD_ARGS: '--workers 4', APS_AUTH_PROVIDER: 'untrusted-provider', HTTP_PROXY: 'http://untrusted.invalid', }, { APS_MODE: 'desktop', CONDA_PREFIX: 'C:\\override-conda', PYTHONPATH: 'override-user-site', PYTHONNOUSERSITE: '0', }, 'win32', ); assert.equal( env.PATH, ['C:\\Windows\\System32', 'C:\\Program Files\\Git\\cmd', 'C:\\APS\\Scripts'].join(';'), ); assert.equal(env.CONDA_PREFIX, undefined); assert.equal(env.CONDA_DEFAULT_ENV, undefined); assert.equal(env.CONDA_EXE, undefined); assert.equal(env.PYTHONHOME, undefined); assert.equal(env.PythonPath, undefined); assert.equal(env.PYTHONPATH, undefined); assert.equal(env.PYTHONUSERBASE, undefined); assert.equal(env.WEB_CONCURRENCY, undefined); assert.equal(env.UVICORN_WORKERS, undefined); assert.equal(env.GUNICORN_CMD_ARGS, undefined); assert.equal(env.APS_AUTH_PROVIDER, undefined); assert.equal(env.HTTP_PROXY, undefined); assert.equal(env.PYTHONNOUSERSITE, '1'); assert.equal(env.PYTHONUTF8, '1'); assert.equal(env.APS_MODE, 'desktop'); }); test('starts the bundled executable with a same-origin UI and stops it', async () => { const { root, resourcesPath, apsHome } = fixture(); const calls = []; const child = fakeChild(42); const manager = new SidecarManager({ resourcesPath, apsHome, platform: 'win32', getPort: async () => 43123, waitForHealthy: async () => {}, createNonce: () => 'a'.repeat(64), terminateChild: (target) => target.kill(), spawnProcess: (...args) => { calls.push(args); return child; }, }); try { assert.equal(await manager.start(), 'http://127.0.0.1:43123'); assert.equal(calls.length, 1); assert.equal(path.basename(calls[0][0]), 'aps-sidecar.exe'); assert.deepEqual(calls[0][1], []); assert.equal(calls[0][2].env.APS_HOME, apsHome); assert.equal(calls[0][2].env.APS_MODE, 'desktop'); assert.equal(calls[0][2].env.APS_API_PORT, '43123'); assert.equal(calls[0][2].env.APS_UI_DIR, path.join(resourcesPath, 'web', 'dist')); assert.equal(calls[0][2].env.APS_SIDECAR_NONCE, 'a'.repeat(64)); assert.equal(calls[0][2].env.APS_PARENT_PID, String(process.pid)); manager.stop(); assert.equal(child.killed, true); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('restarts an unexpectedly exited sidecar with a fresh port and nonce', async () => { const { root, resourcesPath, apsHome } = fixture(); const children = [fakeChild(1), fakeChild(2)]; const ports = [43124, 43125]; const nonces = ['b'.repeat(64), 'c'.repeat(64)]; const calls = []; let launches = 0; const manager = new SidecarManager({ resourcesPath, apsHome, platform: 'win32', getPort: async () => ports.shift(), waitForHealthy: async () => {}, restartDelayMs: 1, stabilityResetMs: 10_000, createNonce: () => nonces.shift(), terminateChild: (target) => target.kill(), spawnProcess: (...args) => { calls.push(args); return children[launches++]; }, }); try { await manager.start(); const restarted = once(manager, 'restarted'); children[0].emit('exit', 7, null); const [event] = await restarted; assert.equal(event.attempt, 1); assert.equal(launches, 2); assert.equal(manager.origin, 'http://127.0.0.1:43125'); assert.equal(calls[0][2].env.APS_API_PORT, '43124'); assert.equal(calls[0][2].env.APS_SIDECAR_NONCE, 'b'.repeat(64)); assert.equal(calls[1][2].env.APS_API_PORT, '43125'); assert.equal(calls[1][2].env.APS_SIDECAR_NONCE, 'c'.repeat(64)); manager.stop(); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('generates a one-time audit token per launch and never leaks it to the child env', async () => { const { root, resourcesPath, apsHome } = fixture(); const children = [fakeChild(1), fakeChild(2)]; const tokens = ['audit-token-1', 'audit-token-2']; const calls = []; let launches = 0; const manager = new SidecarManager({ resourcesPath, apsHome, platform: 'win32', appVersion: '0.2.0', getPort: async () => 43127, waitForHealthy: async () => {}, restartDelayMs: 1, stabilityResetMs: 10_000, createNonce: () => 'a'.repeat(64), createAuditToken: () => tokens.shift(), terminateChild: (target) => target.kill(), spawnProcess: (...args) => { calls.push(args); return children[launches++]; }, }); try { await manager.start(); assert.equal(manager.auditToken, 'audit-token-1'); // 应用版本注入子进程 env;审计 token 仅限 Electron main 进程内,不进 env/不写盘 assert.equal(calls[0][2].env.APS_APP_VERSION, '0.2.0'); assert.equal(calls[0][2].env.APS_AUDIT_TOKEN, undefined); assert.equal(calls[0][2].env.APS_SIDECAR_NONCE, 'a'.repeat(64)); const restarted = once(manager, 'restarted'); children[0].emit('exit', 7, null); await restarted; assert.equal(manager.auditToken, 'audit-token-2'); // 重启后一次性 token 重新生成 assert.equal(calls[1][2].env.APS_AUDIT_TOKEN, undefined); manager.stop(); } finally { fs.rmSync(root, { recursive: true, force: true }); } });