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
This commit is contained in:
chick
2026-09-07 01:09:49 +08:00
parent eed134a25c
commit 4f42a6f835
5 changed files with 20 additions and 10 deletions
+3 -1
View File
@@ -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);
+1 -1
View File
@@ -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;
+6 -5
View File
@@ -45,9 +45,9 @@ async function fixture(name = 'cutover'): Promise<CutoverPlan> {
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');
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;
@@ -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);
+1 -1
View File
@@ -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' });