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:
@@ -131,7 +131,9 @@ afterEach(async () => {
|
|||||||
await Promise.all(cleanup.splice(0).map((fn) => fn()));
|
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 () => {
|
it('makes the exact production gateway executable attempt 8788 only with acknowledgement', async () => {
|
||||||
await expectPortFree(8788);
|
await expectPortFree(8788);
|
||||||
const fixture = await listenOn(8788);
|
const fixture = await listenOn(8788);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const STATE_FILE = 'cutover-state.json';
|
|||||||
const GATEWAY_ARGV = [
|
const GATEWAY_ARGV = [
|
||||||
process.execPath,
|
process.execPath,
|
||||||
'--import',
|
'--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'),
|
resolve(process.cwd(), 'apps/api/src/production-gateway-cli.ts'),
|
||||||
] as const;
|
] as const;
|
||||||
const DIGEST = /^[a-f0-9]{64}$/u;
|
const DIGEST = /^[a-f0-9]{64}$/u;
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ async function fixture(name = 'cutover'): Promise<CutoverPlan> {
|
|||||||
drillEvidence: drill,
|
drillEvidence: drill,
|
||||||
drillEvidenceSha256: createHash('sha256').update(drillContent).digest('hex'),
|
drillEvidenceSha256: createHash('sha256').update(drillContent).digest('hex'),
|
||||||
legacyPid: 41,
|
legacyPid: 41,
|
||||||
legacyCommand: '/usr/local/bin/node /absolute/legacy/server.js',
|
legacyCommand: `${process.execPath} /absolute/legacy/server.js`,
|
||||||
legacyIdentity: 'legacy-start-1',
|
legacyIdentity: 'legacy-start-1',
|
||||||
legacyStart: ['/usr/local/bin/node', '/absolute/legacy/server.js'],
|
legacyStart: [process.execPath, '/absolute/legacy/server.js'],
|
||||||
stateDir: join(root, 'state'),
|
stateDir: join(root, 'state'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -130,11 +130,12 @@ describe('cutover validation and reversible orchestration', () => {
|
|||||||
expect(signals).toEqual([41]);
|
expect(signals).toEqual([41]);
|
||||||
expect(starts[0]?.environment?.MULTI_SIMADMIN_GATEWAY_TOKEN).toBe(token);
|
expect(starts[0]?.environment?.MULTI_SIMADMIN_GATEWAY_TOKEN).toBe(token);
|
||||||
const statePath = join(plan.stateDir, 'cutover-state.json');
|
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 readFile(statePath, 'utf8')).not.toContain(token);
|
||||||
expect(await rollback(plan, system)).toBe(43);
|
expect(await rollback(plan, system)).toBe(43);
|
||||||
expect(signals).toEqual([41, 42]);
|
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 () => {
|
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,
|
process.execPath,
|
||||||
'--import',
|
'--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'),
|
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 {
|
const state = JSON.parse(await readFile(join(plan.stateDir, 'cutover-state.json'), 'utf8')) as {
|
||||||
phase: string;
|
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 files = (await jsonFiles(fixtureRoot)).filter((x) => !x.includes('/synthetic-errors/'));
|
||||||
const srcEntries = await readdir(path.join(root, 'src'));
|
const srcEntries = await readdir(path.join(root, 'src'));
|
||||||
assert.ok(!srcEntries.includes('synthetic-errors'));
|
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 privateValues = stringLeaves(config);
|
||||||
const expected = selectReadonlyOperations(upstream58e2204Operations).selected;
|
const expected = selectReadonlyOperations(upstream58e2204Operations).selected;
|
||||||
assert.equal(files.length, expected.length * 2);
|
assert.equal(files.length, expected.length * 2);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { test } from 'node:test';
|
|||||||
const installer = new URL('../scripts/install.sh', import.meta.url);
|
const installer = new URL('../scripts/install.sh', import.meta.url);
|
||||||
const source = readFileSync(installer, 'utf8');
|
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]);
|
execFileSync('/bin/sh', ['-n', installer.pathname]);
|
||||||
const help = execFileSync('/bin/sh', [installer.pathname, 'help'], { encoding: 'utf8' });
|
const help = execFileSync('/bin/sh', [installer.pathname, 'help'], { encoding: 'utf8' });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user