mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
* fix(patterns): make reflections/patterns slug sub-paths configurable gatherReflections()'s SQL WHERE clause and the pattern-page write slug were hardcoded to wiki/personal/reflections/ and wiki/personal/patterns/ respectively. A prior fix (#2415/#2939) made the leading namespace root configurable via dream.synthesize.output_root, but the personal/reflections and personal/patterns sub-path segments stayed pinned literals, so brains whose schema has no personal/ nesting (e.g. a flat meetings/ tree) could not point the phase at their own compiled_truth source. Adds two new config keys: - dream.patterns.source_slug_prefix (default: <output_root>/personal/reflections) - dream.patterns.output_slug_prefix (default: <output_root>/personal/patterns) Both default to the exact literal the code previously hardcoded, so existing installs see no behavior change. A custom output_slug_prefix is also added to the subagent's put_page allow-list, since the filing-rules JSON globs only remap the wiki/personal/patterns/* literal by output_root and would otherwise reject writes to a differently-shaped output path. Updated test/cycle-patterns.test.ts's scope-filter assertions to match; added coverage for the two new config keys and the allow-list addition. * fix(patterns): drain PGLite subagent job inline (no worker claims it) runPhasePatterns submitted a subagent job via queue.add() and waited on it via waitForCompletion, but on PGLite there is no separate Minions worker process (the embedded data-dir holds an exclusive file lock; 'gbrain jobs work' refuses to start against it). synthesize.ts already has runPgliteSubagentsInline to drive the claim -> run -> complete loop inline for exactly this reason; patterns.ts never called it, so a real (non-dry-run) invocation against a PGLite brain always hung until subagentWaitTimeoutMs (default 35 min) with the job stuck in 'waiting'. Exports runPgliteSubagentsInline from synthesize.ts (was test-only via __testing) and calls it from patterns.ts with the same private per-run childQueueName derivation synthesize.ts uses, so the inline drain never claims unrelated 'default'-queue jobs a Postgres worker owns. Updated test/cycle-patterns-child-outcome.test.ts's #2782 regression test: its premise (no worker running with a 1ms wait timeout, so the job never completes and waitForCompletion genuinely times out) is exactly the scenario this fix addresses. With the inline drain, a fake ANTHROPIC_API_KEY test fixture now gets claimed and actually attempted, failing fast and landing the job in 'dead' rather than staying uncompleted until a timeout. The #2782 status-reflects-outcome contract the test exists to pin is unchanged (any non-'complete' outcome with zero writes still surfaces as status 'fail'); updated the expected outcome/error code to match the outcome that now actually occurs. * feat(think): surface usage/cost_usd in --json output think's own cost was previously unsurfaced anywhere: not in this CLI's own --json output, not in budget_ledger (nothing in src/core/think/*.ts ever writes to it), and invisible to a wrapping caller's own token accounting since the LLM call think makes is its own, separate API call from anything the caller's session tracks. runThink() already captured result.usage.{input_tokens,output_tokens} from the underlying client.create() call but discarded it. Adds usage/cost_usd to ThinkResult, populates usage on the real-LLM-call path (undefined on the no-client/stub paths, matching how synthesisOk already distinguishes those), and computes cost_usd in think.ts's CLI handler via the existing canonicalLookup() pricing table (same pattern brain-score-recommendations.ts's estimateAnthropicCost already uses). Extracted the multiply-and-sum into a small exported computeThinkCostUsd for direct unit testing. Also appends the cost to the human-readable footer. Verified live: gbrain think --json against a real anchor returned usage:{input_tokens:3271,output_tokens:1490}, cost_usd:0.0536, matching Opus pricing ($5/$25 per MTok) by hand calculation.
117 lines
4.8 KiB
TypeScript
117 lines
4.8 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 <source_slug_prefix>/%', () => {
|
|
// #2415 made the top-level namespace root configurable
|
|
// (dream.synthesize.output_root, default 'wiki'). A later patch made the
|
|
// full `personal/reflections` sub-path configurable too
|
|
// (dream.patterns.source_slug_prefix, defaults to
|
|
// `<output_root>/personal/reflections` so existing behavior is
|
|
// unchanged) — schemas with no `personal/` nesting (e.g. a flat
|
|
// `meetings/` tree) can point the phase at their own compiled_truth
|
|
// source instead.
|
|
expect(patternsSrc).toContain('slug LIKE $2');
|
|
expect(patternsSrc).toContain('${sourceSlugPrefix}/%');
|
|
expect(patternsSrc).toContain('dream.patterns.source_slug_prefix');
|
|
});
|
|
|
|
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');
|
|
});
|
|
|
|
test('output slug prefix is config-driven, defaulting to <output_root>/personal/patterns', () => {
|
|
expect(patternsSrc).toContain('dream.patterns.output_slug_prefix');
|
|
expect(patternsSrc).toContain('${outputRoot}/personal/patterns');
|
|
});
|
|
|
|
test('source slug prefix defaults to <output_root>/personal/reflections', () => {
|
|
expect(patternsSrc).toContain('${outputRoot}/personal/reflections');
|
|
});
|
|
|
|
test('adds a configured output_slug_prefix to the subagent write allow-list', () => {
|
|
// A custom dream.patterns.output_slug_prefix (e.g. a flat schema with no
|
|
// personal/ nesting) is not covered by the filing-rules globs, which only
|
|
// remap the `wiki/personal/patterns/*` literal by output_root. The phase
|
|
// must add it explicitly so put_page actually grants write access there.
|
|
expect(patternsSrc).toContain('outputGlob');
|
|
expect(patternsSrc).toContain('allowedSlugPrefixes.push(outputGlob)');
|
|
});
|
|
});
|