diff --git a/src/core/model-config.ts b/src/core/model-config.ts index d99d1b185..ba6ce9db6 100644 --- a/src/core/model-config.ts +++ b/src/core/model-config.ts @@ -47,13 +47,18 @@ export interface ResolveModelOpts { fallback: string; } -/** Default aliases shipped in code. Users override via `models.aliases.` config. */ +/** Default aliases shipped in code. Users override via `models.aliases.` 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 = { - 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 = { * Users override via `gbrain config set models.tier. `. */ export const TIER_DEFAULTS: Record = { - 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', }; /** diff --git a/test/e2e/autopilot-fanout-postgres.test.ts b/test/e2e/autopilot-fanout-postgres.test.ts index 38a16ceff..3cc2f2779 100644 --- a/test/e2e/autopilot-fanout-postgres.test.ts +++ b/test/e2e/autopilot-fanout-postgres.test.ts @@ -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, }); diff --git a/test/e2e/fresh-install-pglite.test.ts b/test/e2e/fresh-install-pglite.test.ts index d2836cc6b..1b3f3f3bc 100644 --- a/test/e2e/fresh-install-pglite.test.ts +++ b/test/e2e/fresh-install-pglite.test.ts @@ -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({ diff --git a/test/e2e/ingestion-roundtrip.test.ts b/test/e2e/ingestion-roundtrip.test.ts index 705c7c59f..23fedbcad 100644 --- a/test/e2e/ingestion-roundtrip.test.ts +++ b/test/e2e/ingestion-roundtrip.test.ts @@ -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');