test: add rollout smoke and restore evidence runbook
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { createServer } from 'node:net';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const capabilities = 'asset_search,asset_valuation';
|
||||
const safeEnv = { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: process.env.HOME ?? '/tmp', TMPDIR: process.env.TMPDIR ?? '/tmp' };
|
||||
async function freePort(): Promise<number> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const probe = createServer();
|
||||
probe.once('error', reject);
|
||||
probe.listen(0, '127.0.0.1', () => {
|
||||
const address = probe.address();
|
||||
const port = typeof address === 'object' && address ? address.port : 0;
|
||||
probe.close((error) => error ? reject(error) : resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const releaseId = `gate-${crypto.randomUUID()}`;
|
||||
const referenceDir = await mkdtemp(join(tmpdir(), 'eden-gate-reference-'));
|
||||
let server: Bun.Subprocess | undefined;
|
||||
async function run(args: string[], env: Record<string, string>): Promise<void> {
|
||||
const child = Bun.spawn(args, { env: { ...safeEnv, ...env }, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
|
||||
if (await child.exited !== 0) throw new Error(`Command failed: ${args.join(' ')}`);
|
||||
}
|
||||
async function waitForServer(): Promise<void> {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(base);
|
||||
if (response.status === 200 && response.headers.get('x-application-release') === releaseId) return;
|
||||
} catch { /* startup race */ }
|
||||
await Bun.sleep(250);
|
||||
}
|
||||
throw new Error('Rollout-gate smoke server did not become ready');
|
||||
}
|
||||
function assertStatus(response: Response, expected: number, label: string): void {
|
||||
if (response.status !== expected) throw new Error(`${label}: expected ${expected}, got ${response.status}`);
|
||||
}
|
||||
try {
|
||||
await run(['bun', 'workers/reference.ts', '--fixture', '--directory', referenceDir], { APP_MODE: 'demo', REFERENCE_DIR: referenceDir });
|
||||
server = Bun.spawn(['bun', 'build/index.js'], {
|
||||
env: {
|
||||
...safeEnv, APP_MODE: 'demo', APP_ORIGIN: base, ORIGIN: base, HOST: '127.0.0.1', PORT: String(port),
|
||||
REFERENCE_DIR: referenceDir, APP_RELEASE_ID: releaseId, APP_CAPABILITY_ALLOWLIST: capabilities,
|
||||
TYPESAFE_API_KEY: 'sentinel-not-used-when-assistant-is-disabled',
|
||||
}, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore',
|
||||
});
|
||||
await waitForServer();
|
||||
const headers = { origin: base, 'content-type': 'application/json' };
|
||||
const chat = await fetch(`${base}/api/chat`, { method: 'POST', headers, body: JSON.stringify({ message: '/value', requestId: crypto.randomUUID() }) });
|
||||
assertStatus(chat, 503, 'assistant dispatch');
|
||||
if (!(await chat.text()).includes('assistant_dispatch')) throw new Error('assistant gate reason missing');
|
||||
const sync = await fetch(`${base}/api/sync`, { method: 'POST', headers: { origin: base } });
|
||||
assertStatus(sync, 503, 'asset sync');
|
||||
const token = await fetch(`${base}/api/session/token`);
|
||||
assertStatus(token, 503, 'session token');
|
||||
const characters = await fetch(`${base}/characters`);
|
||||
assertStatus(characters, 200, 'character page');
|
||||
if (!(await characters.text()).includes('identity')) throw new Error('identity gate reason missing');
|
||||
const oauth = await fetch(`${base}/auth/eve`, { method: 'POST', headers: { origin: base, 'content-type': 'application/x-www-form-urlencoded' }, redirect: 'manual' });
|
||||
assertStatus(oauth, 503, 'OAuth initiation');
|
||||
const callback = await fetch(`${base}/auth/eve/callback?state=synthetic`, { redirect: 'manual' });
|
||||
assertStatus(callback, 303, 'OAuth callback');
|
||||
if (!callback.headers.get('location')?.includes('auth=disabled')) throw new Error('OAuth callback gate reason missing');
|
||||
console.log(`Rollout-gate HTTP smoke passed: disabled capabilities returned fail-closed responses; ${base}`);
|
||||
} finally {
|
||||
if (server) { server.kill('SIGTERM'); await server.exited; }
|
||||
await rm(referenceDir, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user