93 lines
4.0 KiB
TypeScript
93 lines
4.0 KiB
TypeScript
import { createServer } from 'node:net';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
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 configuredPort = process.env.SMOKE_WEB_PORT;
|
|
const port = configuredPort === undefined ? await freePort() : Number(configuredPort);
|
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error('SMOKE_WEB_PORT must be a valid TCP port');
|
|
const base = `http://127.0.0.1:${port}`;
|
|
const releaseId = `fixture-${crypto.randomUUID()}`;
|
|
const root = await mkdtemp(join(tmpdir(), 'eden-fixture-'));
|
|
let server: Bun.Subprocess | undefined;
|
|
// Never pass ambient vault/session/provider variables into repository-controlled
|
|
// fixture processes. The fixture only needs a minimal, non-secret environment.
|
|
const safeEnv = {
|
|
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
HOME: process.env.HOME ?? '/tmp',
|
|
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
};
|
|
|
|
async function compileFixture(directory: string): Promise<void> {
|
|
const child = Bun.spawn(['bun', 'workers/reference.ts', '--fixture', '--directory', directory], {
|
|
stdout: 'pipe', stderr: 'pipe', stdin: 'ignore',
|
|
env: { ...safeEnv, APP_MODE: 'demo', REFERENCE_DIR: directory },
|
|
});
|
|
const code = await child.exited;
|
|
if (code !== 0) throw new Error('Fixture reference compilation failed');
|
|
}
|
|
|
|
async function waitForServer(expectedReleaseId: string): 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') === expectedReleaseId) return;
|
|
} catch { /* startup race */ }
|
|
await Bun.sleep(250);
|
|
}
|
|
throw new Error('Fixture web server did not become ready');
|
|
}
|
|
|
|
try {
|
|
await compileFixture(root);
|
|
server = Bun.spawn(['bun', 'build/index.js'], {
|
|
stdout: 'pipe', stderr: 'pipe', stdin: 'ignore',
|
|
env: {
|
|
...safeEnv,
|
|
APP_MODE: 'demo', APP_ORIGIN: base, ORIGIN: base,
|
|
HOST: '127.0.0.1', PORT: String(port), REFERENCE_DIR: root,
|
|
APP_RELEASE_ID: releaseId,
|
|
APP_CAPABILITY_ALLOWLIST: 'asset_search,asset_valuation,asset_sync,assistant_dispatch,session_token,identity,subscriptions',
|
|
},
|
|
});
|
|
await waitForServer(releaseId);
|
|
|
|
const rootResponse = await fetch(base);
|
|
if (!(await rootResponse.text()).includes('FIXTURE DEMO')) throw new Error('Fixture label missing');
|
|
if (rootResponse.headers.get('x-application-release') !== releaseId) throw new Error('Release identifier header missing');
|
|
const assets = await fetch(`${base}/assets`);
|
|
if (assets.status !== 200 || !(await assets.text()).includes('2,086,000.00')) throw new Error('Fixture asset report failed');
|
|
|
|
for (const message of ['/value', '/assets']) {
|
|
const response = await fetch(`${base}/api/chat`, {
|
|
method: 'POST', headers: { origin: base, 'content-type': 'application/json' },
|
|
body: JSON.stringify({ message, requestId: crypto.randomUUID() }),
|
|
});
|
|
if (response.status !== 200) throw new Error(`Fixture assistant failed for ${message}`);
|
|
const result = await response.json() as { status?: string; operation?: { status?: string; report?: { total?: string } } };
|
|
if (result.status !== 'ready' || result.operation?.status !== 'ready' || result.operation.report?.total !== '208600000') {
|
|
throw new Error(`Fixture assistant returned an unexpected result for ${message}`);
|
|
}
|
|
}
|
|
console.log(`Isolated fixture smoke passed: ${base}; synthetic reference/backend only`);
|
|
} finally {
|
|
if (server) {
|
|
server.kill('SIGTERM');
|
|
await server.exited;
|
|
}
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|