fix(e2e): 4 root-cause fixes for pre-existing E2E flakes (master polish)

After merging origin/master (which landed v0.40.8.0's flake-fix wave),
re-ran the 6 E2E files previously called out as pre-existing failures.
v0.40.8.0 had already fixed 3; the remaining 3 had real root causes:

1. autopilot-fanout-postgres — hardcoded date 2026-05-22 was 30min ago
   when the test was written; today (2026-05-24) it's 2 days past the
   60-min freshness window. selectSourcesForDispatch correctly classifies
   the source as STALE (dispatch.length=1) instead of FRESH (length=0).
   Fix: replace literal date with Date.now() - 30 * 60 * 1000 so the
   timestamp stays relative-fresh forever.

2. ingestion-roundtrip — chokidar cross-test contamination on macOS
   FSEvents. Tests share OS-level fd resources across describe blocks;
   the first test's watcher hasn't fully released when the second
   test's watcher attaches, so the new watcher's events queue behind
   pending cleanup and the waitFor(15s) for the first file drop times
   out. Fixes:
     - Move fs.mkdirSync(inboxDir) BEFORE createInboxFolderSource +
       daemon.start to eliminate the chokidar attach race (chokidar
       can watch non-existent dirs but the timing is unreliable
       under test load).
     - Add 200ms grace period in beforeEach after resetPgliteState
       to let prior watchers fully release FSEvents handles.
     - mkdirSync both inboxA + inboxB BEFORE source registration in
       the multi-source test (same race shape).
     - Bump waitFor timeouts 6s → 15s for fs.watch flake tolerance.

3. fresh-install-pglite — dev machines with multi-provider env
   (OPENAI_API_KEY + VOYAGE_API_KEY + ZEROENTROPY_API_KEY set in zsh)
   fail init's disambiguation gate with "Multiple embedding providers
   env-ready". The test sets ZE_API_KEY but doesn't NEGATE the others.
   Fix: beforeEach saves + clears OPENAI_API_KEY + VOYAGE_API_KEY so
   init sees only ZE. afterEach restores. Hermetic per dev machine.

4. dream-synthesize-chunking — TIER_DEFAULTS + DEFAULT_ALIASES in
   src/core/model-config.ts had BARE Anthropic model ids (e.g.
   'claude-sonnet-4-6' instead of 'anthropic:claude-sonnet-4-6'). The
   v0.40.8+ subagent queue's classifyCapabilities() now validates that
   submitted models have a provider prefix via resolveRecipe(), which
   throws "unknown provider" on bare ids. The synthesize phase
   resolveModel → bare 'claude-sonnet-4-6' → submit_job → REJECT →
   phase 'fail' status with empty details (test expected children_submitted=1).
   Fix: prefix all 4 TIER_DEFAULTS + 5 DEFAULT_ALIASES with their
   provider (anthropic:claude-*, google:gemini-3-pro, openai:gpt-5).
   Production paths already worked because user pack manifests have
   explicit `models.tier.subagent = anthropic:...`; only the fallback
   path (used in tests with no API key + no model config) hit the
   bare-id format and broke.

Verification (all run against DATABASE_URL=...:5434/gbrain_test):
  test/e2e/autopilot-fanout-postgres.test.ts → 6/6 pass
  test/e2e/dream-cycle-phase-order-pglite.test.ts → 5/5 pass
  test/e2e/dream-synthesize-chunking.test.ts → 4/4 pass
  test/e2e/fresh-install-pglite.test.ts → 2/2 pass
  test/e2e/http-transport.test.ts → 8/8 pass
  test/e2e/ingestion-roundtrip.test.ts → 3/3 pass
  test/e2e/mechanical.test.ts → 78/78 pass
  Total: 106/106 pass, 0 fail.

Adjacent unit tests verified green:
  test/anthropic-model-ids.test.ts → 6/6 pass
  test/model-config.serial.test.ts → 19/19 pass

typecheck clean.

Plan: v0.41 wave (~/.claude/plans/system-instruction-you-are-working-toasty-milner.md).
Post-merge polish — every E2E failure surfaced in the v0.41 ship reports is now green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-24 08:59:26 -07:00
co-authored by Claude Opus 4.7
parent b355715b68
commit c4f03a9df6
4 changed files with 58 additions and 18 deletions
+15 -10
View File
@@ -47,13 +47,18 @@ export interface ResolveModelOpts {
fallback: string;
}
/** Default aliases shipped in code. Users override via `models.aliases.<name>` config. */
/** Default aliases shipped in code. Users override via `models.aliases.<name>` config.
* Values include the `provider:` prefix so resolved model strings always
* carry an explicit provider — required by the v0.40.8+ subagent queue's
* classifyCapabilities() validation. Bare model ids (e.g. `claude-opus-4-7`)
* cause `resolveRecipe()` to throw "unknown provider" and the queue rejects
* the submit. */
export const DEFAULT_ALIASES: Record<string, string> = {
opus: 'claude-opus-4-7',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
gemini: 'gemini-3-pro',
gpt: 'gpt-5',
opus: 'anthropic:claude-opus-4-7',
sonnet: 'anthropic:claude-sonnet-4-6',
haiku: 'anthropic:claude-haiku-4-5-20251001',
gemini: 'google:gemini-3-pro',
gpt: 'openai:gpt-5',
};
/**
@@ -66,10 +71,10 @@ export const DEFAULT_ALIASES: Record<string, string> = {
* Users override via `gbrain config set models.tier.<tier> <model>`.
*/
export const TIER_DEFAULTS: Record<ModelTier, string> = {
utility: 'claude-haiku-4-5-20251001',
reasoning: 'claude-sonnet-4-6',
deep: 'claude-opus-4-7',
subagent: 'claude-sonnet-4-6',
utility: 'anthropic:claude-haiku-4-5-20251001',
reasoning: 'anthropic:claude-sonnet-4-6',
deep: 'anthropic:claude-opus-4-7',
subagent: 'anthropic:claude-sonnet-4-6',
};
/**
+7 -1
View File
@@ -144,7 +144,13 @@ describeIfDB('autopilot fan-out — Postgres E2E', () => {
await seedSource('full-round-trip');
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
const ts = '2026-05-22T15:00:00.000Z';
// Use a recent (within-freshness-window) timestamp so the source
// classifies as fresh. Hardcoded dates rot — when this test was
// written, '2026-05-22T15:00:00.000Z' was 30 minutes ago and within
// the window. Two days later it's past the window and the source
// dispatches instead of being skipped, breaking the assertion on
// line below. Relative timestamp keeps the test valid forever.
const ts = new Date(Date.now() - 30 * 60 * 1000).toISOString();
const updated = await engine.updateSourceConfig('full-round-trip', {
last_full_cycle_at: ts,
});
+13
View File
@@ -27,11 +27,22 @@ describe('E2E: fresh gbrain init --pglite → import → embed works end-to-end'
let tmpHome: string;
let origHome: string | undefined;
let origZeKey: string | undefined;
let origOpenaiKey: string | undefined;
let origVoyageKey: string | undefined;
beforeEach(() => {
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-e2e-fresh-'));
origHome = process.env.GBRAIN_HOME;
origZeKey = process.env.ZEROENTROPY_API_KEY;
// Save + clear OPENAI_API_KEY + VOYAGE_API_KEY so init only sees
// one provider as env-ready (ZE). Without this, dev machines with
// multi-provider env (Garry's setup) fail init's disambiguation gate
// ("Multiple embedding providers env-ready: openai, voyage,
// zeroentropyai") before the test body runs.
origOpenaiKey = process.env.OPENAI_API_KEY;
origVoyageKey = process.env.VOYAGE_API_KEY;
delete process.env.OPENAI_API_KEY;
delete process.env.VOYAGE_API_KEY;
process.env.GBRAIN_HOME = tmpHome;
// Stub key so init's setup-hint check passes.
process.env.ZEROENTROPY_API_KEY = 'sk-test-ze';
@@ -43,6 +54,8 @@ describe('E2E: fresh gbrain init --pglite → import → embed works end-to-end'
else process.env.GBRAIN_HOME = origHome;
if (origZeKey === undefined) delete process.env.ZEROENTROPY_API_KEY;
else process.env.ZEROENTROPY_API_KEY = origZeKey;
if (origOpenaiKey !== undefined) process.env.OPENAI_API_KEY = origOpenaiKey;
if (origVoyageKey !== undefined) process.env.VOYAGE_API_KEY = origVoyageKey;
__setEmbedTransportForTests(null);
// Restore legacy-preload gateway state.
configureGateway({
+23 -7
View File
@@ -66,6 +66,12 @@ afterAll(async () => {
beforeEach(async () => {
await resetPgliteState(engine);
// 200ms grace period for the previous test's chokidar watchers to fully
// release OS-level FSEvents handles on macOS. Without this, the second
// test's watcher events queue behind the first test's pending cleanup
// and the waitFor(15s) for the first file drop times out. See
// ingestion-roundtrip cross-test contamination notes.
await new Promise((r) => setTimeout(r, 200));
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-e2e-roundtrip-'));
inboxDir = path.join(tmpRoot, 'inbox');
brainDir = path.join(tmpRoot, 'brain');
@@ -117,6 +123,12 @@ describe('ingestion roundtrip — inbox-folder → daemon → ingest_capture →
},
});
// Create the inbox dir BEFORE starting the watcher to eliminate a race
// where chokidar hasn't attached yet when the first write fires (the
// 6s→15s waitFor flake on the source.) Without this, the test relies on
// chokidar's polling fallback to notice the dir, which is timing-dependent.
fs.mkdirSync(inboxDir, { recursive: true });
const source = createInboxFolderSource({
inboxDir,
debounceMs: 50,
@@ -126,12 +138,11 @@ describe('ingestion roundtrip — inbox-folder → daemon → ingest_capture →
await daemon.start();
// Drop a file into the inbox.
fs.mkdirSync(inboxDir, { recursive: true });
const captured = path.join(inboxDir, 'roundtrip.md');
fs.writeFileSync(captured, '---\ntitle: Roundtrip\n---\n\nfull e2e flow');
// Wait for the daemon to pick it up + dispatch + handler to write.
await waitFor(() => dispatchedEvents.length === 1, 6000);
await waitFor(() => dispatchedEvents.length === 1, 15000);
// Page is in the DB.
const page = await engine.getPage(dispatchedEvents[0]!.metadata!.slug as string ??
@@ -168,6 +179,9 @@ describe('ingestion roundtrip — inbox-folder → daemon → ingest_capture →
},
});
// mkdirSync BEFORE daemon.start to eliminate chokidar attach race.
fs.mkdirSync(inboxDir, { recursive: true });
const source = createInboxFolderSource({
inboxDir,
debounceMs: 50,
@@ -176,12 +190,10 @@ describe('ingestion roundtrip — inbox-folder → daemon → ingest_capture →
daemon.register({ source });
await daemon.start();
fs.mkdirSync(inboxDir, { recursive: true });
// Drop file 1
const drop1 = path.join(inboxDir, 'dup-1.md');
fs.writeFileSync(drop1, '# duplicate content\n\nidentical body');
await waitFor(() => dispatchedEvents.length === 1, 6000);
await waitFor(() => dispatchedEvents.length === 1, 15000);
// Drop file 2 with byte-identical content (different filename).
const drop2 = path.join(inboxDir, 'dup-2.md');
@@ -217,9 +229,13 @@ describe('ingestion roundtrip — multi-source coordination', () => {
},
});
// Two distinct inbox dirs, two sources.
// Two distinct inbox dirs, two sources. Create the dirs BEFORE
// daemon.start to eliminate the chokidar attach race (same fix as
// the single-source tests above).
const inboxA = path.join(tmpRoot, 'inbox-a');
const inboxB = path.join(tmpRoot, 'inbox-b');
fs.mkdirSync(inboxA, { recursive: true });
fs.mkdirSync(inboxB, { recursive: true });
const sourceA = createInboxFolderSource({
id: 'inbox-a',
inboxDir: inboxA,
@@ -239,7 +255,7 @@ describe('ingestion roundtrip — multi-source coordination', () => {
fs.writeFileSync(path.join(inboxA, 'from-a.md'), 'content from A');
fs.writeFileSync(path.join(inboxB, 'from-b.md'), 'content from B');
await waitFor(() => dispatchedEvents.length === 2, 6000);
await waitFor(() => dispatchedEvents.length === 2, 15000);
const fromA = dispatchedEvents.find((e) => e.source_id === 'inbox-a');
const fromB = dispatchedEvents.find((e) => e.source_id === 'inbox-b');