Files
gbrain/test/cycle-patterns.test.ts
T
c46b457978 fix(dream): gate patterns phase on gateway provider reachability, not ANTHROPIC_API_KEY (takeover of #2279)
Absorbs PR #2279's intent (drop the hardcoded ANTHROPIC_API_KEY gate) with
the end-state the gateway world actually wants: the patterns phase now
probes the RESOLVED patterns model through probeChatModel(normalizeModelId)
— the same semantics as think/index.ts and synthesize's makeJudgeClient.

Fixes two misclassifications of the old env gate:
- Non-Anthropic stacks (litellm, deepseek, openrouter, ...) were skipped as
  "no upstream" even though the subagent routes them through the gateway
  (agent.use_gateway_loop). They now pass the gate; their auth is checked
  lazily at dispatch and surfaces in the job outcome.
- Anthropic keys set via `gbrain config set anthropic_api_key` (stdio MCP
  launches without shell env) were treated as missing. hasAnthropicKey
  inside probeChatModel reads both sources.

Skip reason renames no_api_key → no_provider (carrying the probe's detail).
Both pinning tests updated: the structural test asserts the probe wiring;
the PGLite E2E swaps its env-only helper for the shared hermetic
withoutAnthropicKey (env + config file) so it can't flip to a live LLM call
on a dev machine with a config-file key.

Co-authored-by: brettdavies <brettdavies@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:12:31 -07:00

89 lines
3.3 KiB
TypeScript

/**
* Unit tests for the patterns phase (v0.21).
*
* The phase invokes a subagent and queues real Minions work, so this
* file leans on structural assertions over the source + a single
* end-to-end driver run that exercises the skip-paths.
*
* Full LLM behavior is exercised by E2E tests in test/e2e/.
*/
import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'fs';
const patternsSrc = readFileSync(
new URL('../src/core/cycle/patterns.ts', import.meta.url),
'utf-8',
);
describe('patterns phase wiring', () => {
test('imports queue + waitForCompletion + types', () => {
expect(patternsSrc).toContain("import { MinionQueue }");
expect(patternsSrc).toContain('waitForCompletion');
expect(patternsSrc).toContain('SubagentHandlerData');
});
test('threads allowed_slug_prefixes from filing-rules JSON', () => {
expect(patternsSrc).toContain('allowed_slug_prefixes');
expect(patternsSrc).toContain('_brain-filing-rules.json');
expect(patternsSrc).toContain('dream_synthesize_paths');
});
test('reads min_evidence + lookback_days config', () => {
expect(patternsSrc).toContain('dream.patterns.min_evidence');
expect(patternsSrc).toContain('dream.patterns.lookback_days');
});
test('uses subagent_tool_executions for slug provenance (Codex #2 fix)', () => {
expect(patternsSrc).toContain('subagent_tool_executions');
expect(patternsSrc).toContain("tool_name = 'brain_put_page'");
});
test('gates on gateway provider reachability, not ANTHROPIC_API_KEY (PR #2279)', () => {
// The gate must probe the RESOLVED patterns model through the gateway
// (any configured provider can run patterns), not hardcode the Anthropic
// env var — that misclassified non-Anthropic stacks as "no upstream".
expect(patternsSrc).toContain('probeChatModel');
expect(patternsSrc).toContain('normalizeModelId');
expect(patternsSrc).toContain('no_provider');
expect(patternsSrc).not.toContain('process.env.ANTHROPIC_API_KEY');
});
test('skips when reflections below min_evidence', () => {
expect(patternsSrc).toContain('insufficient_evidence');
});
test('reverse-writes pages to disk via serializeMarkdown', () => {
expect(patternsSrc).toContain('serializeMarkdown');
expect(patternsSrc).toContain('writeFileSync');
});
test('runs after extract — queries fresh graph', () => {
// Documented invariant: pattern phase MUST run after extract.
// The cycle.ts dispatcher enforces order; this just confirms the
// patterns module doesn't try to compute its own auto-link layer
// (which would be a subtle regression).
expect(patternsSrc).not.toContain('runAutoLink');
expect(patternsSrc).not.toContain('extractPageLinks(');
});
test('does NOT use raw_data table (Codex #3 fix)', () => {
expect(patternsSrc).not.toContain('putRawData');
expect(patternsSrc).not.toContain('getRawData');
});
});
describe('patterns scope filter', () => {
test('filters reflections by slug LIKE wiki/personal/reflections/%', () => {
expect(patternsSrc).toContain("slug LIKE 'wiki/personal/reflections/%'");
});
test('orders by updated_at DESC for recency-bias', () => {
expect(patternsSrc).toContain('ORDER BY updated_at DESC');
});
test('caps gather to 100 reflections (cost control)', () => {
expect(patternsSrc).toContain('LIMIT 100');
});
});