mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
The `[P0] cold first Home run` packaged mac case has been red on almost
every beta build for eleven days (1 pass in 12 attempts) and nobody saw
it, because release-beta's smoke step is `continue-on-error: true`.
Four changes, in the order they matter:
1. Capture evidence before cleanup. A failing case uninstalled the app
and the next case deleted the runtime namespace, so the only surviving
trace was `logs: {skipped: true}`. The failure path now writes the
desktop log, a raw `/api/runs` diagnostics snapshot, and the error into
the release report before anything is torn down.
2. Give the wait a budget that matches the chain, split in two. 15s had to
cover cold project creation through rendered output; it is now 60s for
"the daemon owns a finished run" plus 30s for "the output surfaced", so
a timeout says which half broke instead of leaving three observations
that cannot falsify each other. Polling moved inside the page: every
sample used to cost a `tools-pack mac inspect` process, which ate about
a third of the old budget on its own.
3. Drop the injected 503. `POST /api/runs` does not answer
WORKSPACE_AUTHORITY_UNAVAILABLE — local run creation is deliberately
independent of Workspace membership availability — so the case spent
an extra create round trip plus a 500ms backoff on a branch production
cannot reach. Run-create retry semantics stay covered by
apps/web/tests/providers/sse.test.ts.
4. Make the exempted red audible. The exemption stays (at a 1/12 pass rate
a blocking gate would stall the daily beta), but `tools-release
write-report` now turns a failed smoke into a run annotation and a
caution banner at the top of the step summary.
113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
capturePackagedFailureEvidence,
|
|
packagedEvidenceRelpath,
|
|
PACKAGED_FAILURE_EVIDENCE_DIR,
|
|
} from '@/vitest/packaged-failure-evidence';
|
|
import { createReport } from '@/vitest/report';
|
|
|
|
const scratchRoots: string[] = [];
|
|
|
|
async function createScratchReport() {
|
|
const root = await mkdtemp(join(tmpdir(), 'od-packaged-evidence-'));
|
|
scratchRoots.push(root);
|
|
return { report: await createReport(root), root };
|
|
}
|
|
|
|
afterEach(async () => {
|
|
while (scratchRoots.length > 0) {
|
|
const root = scratchRoots.pop();
|
|
if (root != null) await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
describe('packaged failure evidence paths', () => {
|
|
it('keeps every capture inside the report root even for hostile names', () => {
|
|
expect(packagedEvidenceRelpath(PACKAGED_FAILURE_EVIDENCE_DIR, 'runs.json'))
|
|
.toBe('first-run-failure/runs.json');
|
|
expect(packagedEvidenceRelpath('first-run-failure', '../../escape.log'))
|
|
.toBe('first-run-failure/escape.log');
|
|
expect(packagedEvidenceRelpath('first-run-failure', '/etc/passwd'))
|
|
.toBe('first-run-failure/etc-passwd');
|
|
expect(packagedEvidenceRelpath('first-run-failure', '')).toBe('first-run-failure/unnamed');
|
|
});
|
|
});
|
|
|
|
describe('packaged failure evidence capture', () => {
|
|
it('writes every source into the report and records them in a manifest', async () => {
|
|
const { report, root } = await createScratchReport();
|
|
|
|
const entries = await capturePackagedFailureEvidence(report, PACKAGED_FAILURE_EVIDENCE_DIR, [
|
|
{ name: 'error.txt', read: async () => 'stage timed out\n' },
|
|
{ name: 'runs.json', read: async () => '{"runs":[]}\n' },
|
|
{ name: 'desktop-latest.log', read: async () => Uint8Array.from([104, 105]) },
|
|
]);
|
|
|
|
expect(entries.map((entry) => entry.status)).toEqual(['saved', 'saved', 'saved']);
|
|
expect(await readFile(join(root, 'first-run-failure', 'error.txt'), 'utf8'))
|
|
.toBe('stage timed out\n');
|
|
expect(await readFile(join(root, 'first-run-failure', 'desktop-latest.log'), 'utf8')).toBe('hi');
|
|
const manifest = JSON.parse(
|
|
await readFile(join(root, 'first-run-failure', 'index.json'), 'utf8'),
|
|
) as { entries: Array<{ name: string; relpath: string }> };
|
|
expect(manifest.entries.map((entry) => entry.relpath)).toEqual([
|
|
'first-run-failure/error.txt',
|
|
'first-run-failure/runs.json',
|
|
'first-run-failure/desktop-latest.log',
|
|
]);
|
|
});
|
|
|
|
it('keeps capturing after an unreadable source and records why it failed', async () => {
|
|
// A failed packaged case is usually failing *because* something is broken,
|
|
// so the first source that cannot be read is the normal case, not the
|
|
// exceptional one. Losing the remaining evidence to it would reproduce the
|
|
// exact blind spot this capture exists to remove.
|
|
const { report, root } = await createScratchReport();
|
|
|
|
const entries = await capturePackagedFailureEvidence(report, PACKAGED_FAILURE_EVIDENCE_DIR, [
|
|
{
|
|
name: 'runs.json',
|
|
read: async () => {
|
|
throw new Error('desktop IPC socket is gone');
|
|
},
|
|
},
|
|
{ name: 'error.txt', read: async () => 'stage timed out\n' },
|
|
]);
|
|
|
|
expect(entries[0]).toMatchObject({ name: 'runs.json', status: 'failed' });
|
|
expect(entries[0]?.detail).toContain('desktop IPC socket is gone');
|
|
expect(entries[1]).toMatchObject({ name: 'error.txt', status: 'saved' });
|
|
expect(await readFile(join(root, 'first-run-failure', 'error.txt'), 'utf8'))
|
|
.toBe('stage timed out\n');
|
|
});
|
|
|
|
it('never throws out of the cleanup path, even when the report itself is broken', async () => {
|
|
const brokenReport = {
|
|
json: async () => {
|
|
throw new Error('report root is read-only');
|
|
},
|
|
save: async () => {
|
|
throw new Error('report root is read-only');
|
|
},
|
|
};
|
|
|
|
const entries = await capturePackagedFailureEvidence(brokenReport, PACKAGED_FAILURE_EVIDENCE_DIR, [
|
|
{ name: 'error.txt', read: async () => 'stage timed out\n' },
|
|
]);
|
|
|
|
expect(entries).toEqual([
|
|
{
|
|
detail: 'Error: report root is read-only',
|
|
name: 'error.txt',
|
|
relpath: 'first-run-failure/error.txt',
|
|
status: 'failed',
|
|
},
|
|
]);
|
|
});
|
|
});
|