Files
gbrain/test/think-cost.test.ts
T
Ingmar KruschandGitHub dde1bd9353 fix(patterns): make reflections/patterns slug sub-paths configurable (#3389)
* 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.
2026-07-27 14:11:36 -07:00

38 lines
1.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { computeThinkCostUsd } from '../src/commands/think.ts';
// think's own cost was previously unsurfaced anywhere: not in this CLI's own
// --json output, not in budget_ledger, and invisible to a wrapping caller's
// own token accounting (the LLM call think makes is its own, separate API
// call). computeThinkCostUsd is the small, pure function that turns
// {input_tokens, output_tokens} + a resolved modelUsed into a USD figure via
// the existing canonical pricing table.
describe('computeThinkCostUsd', () => {
test('computes cost from real usage against a known model', () => {
const cost = computeThinkCostUsd(
{ input_tokens: 1_000_000, output_tokens: 1_000_000 },
'anthropic:claude-sonnet-5',
);
// sonnet-5 pricing: input $3.00/MTok, output $15.00/MTok.
expect(cost).toBe(18);
});
test('undefined usage (no-client/stub path) → undefined, never a fabricated cost', () => {
expect(computeThinkCostUsd(undefined, 'anthropic:claude-sonnet-5')).toBeUndefined();
});
test('unknown model id → undefined, not zero (no pricing to compute from)', () => {
expect(computeThinkCostUsd(
{ input_tokens: 100, output_tokens: 100 },
'some-unlisted-provider:some-model',
)).toBeUndefined();
});
test('zero-token usage → zero cost, not undefined (a real call that happened to be free)', () => {
expect(computeThinkCostUsd(
{ input_tokens: 0, output_tokens: 0 },
'anthropic:claude-sonnet-5',
)).toBe(0);
});
});