From 4f42a6f835ce5744e30fe0c32cc1401463bb4fe8 Mon Sep 17 00:00:00 2001 From: chick Date: Mon, 7 Sep 2026 01:09:49 +0800 Subject: [PATCH] fix: make cutover restart argv upgrade-safe and tests platform-honest - cutover-orchestrator: restart via 'node --import tsx' (matching install.sh) instead of a hardcoded .pnpm/tsx@4.22.4 loader path that breaks on tsx bumps - production-gateway: use process.execPath for the legacy-start fixture; POSIX-only 0600 mode assertion now win32-guarded - cli-runtime + installer suites: skip where they require POSIX-only process-group SIGTERM, lsof, or /bin/sh - fixture-safety: fall back to config.example.json when config.json is absent on a fresh clone --- apps/api/src/cli-runtime.test.ts | 4 +++- apps/api/src/cutover-orchestrator.ts | 2 +- apps/api/src/production-gateway.test.ts | 13 +++++++------ packages/test-fixtures/test/fixture-safety.test.ts | 9 ++++++++- test/installer.test.js | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/api/src/cli-runtime.test.ts b/apps/api/src/cli-runtime.test.ts index 067eaa8..ef75a87 100644 --- a/apps/api/src/cli-runtime.test.ts +++ b/apps/api/src/cli-runtime.test.ts @@ -131,7 +131,9 @@ afterEach(async () => { await Promise.all(cleanup.splice(0).map((fn) => fn())); }); -describe.sequential('executable package runtimes', () => { +// The runtime lifecycle under test is macOS/POSIX-only: process-group SIGTERM, +// lsof port inspection, and corepack as a direct spawn target. +describe.sequential('executable package runtimes', { skip: process.platform === 'win32' }, () => { it('makes the exact production gateway executable attempt 8788 only with acknowledgement', async () => { await expectPortFree(8788); const fixture = await listenOn(8788); diff --git a/apps/api/src/cutover-orchestrator.ts b/apps/api/src/cutover-orchestrator.ts index 19901e5..d2394df 100644 --- a/apps/api/src/cutover-orchestrator.ts +++ b/apps/api/src/cutover-orchestrator.ts @@ -23,7 +23,7 @@ const STATE_FILE = 'cutover-state.json'; const GATEWAY_ARGV = [ process.execPath, '--import', - resolve(process.cwd(), 'node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/dist/loader.mjs'), + 'tsx', resolve(process.cwd(), 'apps/api/src/production-gateway-cli.ts'), ] as const; const DIGEST = /^[a-f0-9]{64}$/u; diff --git a/apps/api/src/production-gateway.test.ts b/apps/api/src/production-gateway.test.ts index e289022..cabd3cd 100644 --- a/apps/api/src/production-gateway.test.ts +++ b/apps/api/src/production-gateway.test.ts @@ -45,9 +45,9 @@ async function fixture(name = 'cutover'): Promise { drillEvidence: drill, drillEvidenceSha256: createHash('sha256').update(drillContent).digest('hex'), legacyPid: 41, - legacyCommand: '/usr/local/bin/node /absolute/legacy/server.js', + legacyCommand: `${process.execPath} /absolute/legacy/server.js`, legacyIdentity: 'legacy-start-1', - legacyStart: ['/usr/local/bin/node', '/absolute/legacy/server.js'], + legacyStart: [process.execPath, '/absolute/legacy/server.js'], stateDir: join(root, 'state'), }; } @@ -130,11 +130,12 @@ describe('cutover validation and reversible orchestration', () => { expect(signals).toEqual([41]); expect(starts[0]?.environment?.MULTI_SIMADMIN_GATEWAY_TOKEN).toBe(token); const statePath = join(plan.stateDir, 'cutover-state.json'); - expect((await lstat(statePath)).mode & 0o777).toBe(0o600); + if (process.platform !== 'win32') + expect((await lstat(statePath)).mode & 0o777).toBe(0o600); expect(await readFile(statePath, 'utf8')).not.toContain(token); expect(await rollback(plan, system)).toBe(43); expect(signals).toEqual([41, 42]); - expect(starts[1]?.argv).toEqual(['/usr/local/bin/node', '/absolute/legacy/server.js']); + expect(starts[1]?.argv).toEqual([process.execPath, '/absolute/legacy/server.js']); }); it('automatically restarts legacy if the gateway fails to own authenticated 8788', async () => { @@ -146,10 +147,10 @@ describe('cutover validation and reversible orchestration', () => { [ process.execPath, '--import', - resolve(process.cwd(), 'node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/dist/loader.mjs'), + 'tsx', resolve(process.cwd(), 'apps/api/src/production-gateway-cli.ts'), ], - ['/usr/local/bin/node', '/absolute/legacy/server.js'], + [process.execPath, '/absolute/legacy/server.js'], ]); const state = JSON.parse(await readFile(join(plan.stateDir, 'cutover-state.json'), 'utf8')) as { phase: string; diff --git a/packages/test-fixtures/test/fixture-safety.test.ts b/packages/test-fixtures/test/fixture-safety.test.ts index 20bbb11..3628839 100644 --- a/packages/test-fixtures/test/fixture-safety.test.ts +++ b/packages/test-fixtures/test/fixture-safety.test.ts @@ -221,7 +221,14 @@ test('real fixtures satisfy envelope, registry, leak scan, and manifest integrit const files = (await jsonFiles(fixtureRoot)).filter((x) => !x.includes('/synthetic-errors/')); const srcEntries = await readdir(path.join(root, 'src')); assert.ok(!srcEntries.includes('synthetic-errors')); - const config = JSON.parse(await readFile(path.resolve(root, '../../config.json'), 'utf8')); + // config.json is gitignored; on a fresh clone scan the committed example config + // instead so the leak scan still exercises real instance-shaped data. + const configPath = path.resolve(root, '../../config.json'); + const configText = await readFile(configPath, 'utf8').catch((error) => { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + return readFile(path.resolve(root, '../../config.example.json'), 'utf8'); + }); + const config = JSON.parse(configText); const privateValues = stringLeaves(config); const expected = selectReadonlyOperations(upstream58e2204Operations).selected; assert.equal(files.length, expected.length * 2); diff --git a/test/installer.test.js b/test/installer.test.js index a8c056a..43fce48 100644 --- a/test/installer.test.js +++ b/test/installer.test.js @@ -6,7 +6,7 @@ import { test } from 'node:test'; const installer = new URL('../scripts/install.sh', import.meta.url); const source = readFileSync(installer, 'utf8'); -test('one-click installer has safe lifecycle commands and pinned production defaults', () => { +test('one-click installer has safe lifecycle commands and pinned production defaults', { skip: process.platform === 'win32' && 'runs /bin/sh; installer targets macOS' }, () => { execFileSync('/bin/sh', ['-n', installer.pathname]); const help = execFileSync('/bin/sh', [installer.pathname, 'help'], { encoding: 'utf8' });