Files
gbrain/test/config-set.test.ts
e1cefd0654 fix(subagent): orchestration fix-wave G — configurable timeouts/caps, honest child outcomes, fenced timeline writes (#2937)
Four verified-open issues in the subagent-orchestration family, one PR:

- #1594: dream synthesize subagent job/wait timeouts promoted from
  hardcoded 30/35-min constants to config keys
  dream.synthesize.subagent_timeout_ms / subagent_wait_timeout_ms.
  Approach ported from stale PR #1596 (credit @ai920wisco).
- #2778: add_timeline_entry joins the subagent brain-tool allowlist,
  fenced fail-closed by the shared enforceSubagentSlugFence (extracted
  from put_page's inline check — same trusted-workspace allow-list /
  wiki/agents/<id>/ namespace policy). The per-turn output cap is now
  resolveMaxOutputTokens (data.max_tokens → agent.max_output_tokens →
  8192, was hardcoded 4096); a max_tokens stop surfaces as
  stop_reason 'max_tokens' instead of a silent end_turn, and a
  mid-tool-round cap hit injects a truncation note so the model
  re-issues the dropped call.
- #2782: patterns phase status now reflects the child outcome —
  non-complete outcome with zero writes → fail (PATTERNS_CHILD_<OUTCOME>),
  partial writes → warn. Patterns timeouts get the same config-key pair
  (dream.patterns.subagent_timeout_ms / subagent_wait_timeout_ms).
- #2113: facts extraction cap is config facts.extraction_max_tokens
  (default 4000, was hardcoded 1500); stopReason 'length' is checked,
  retried once at 2x the cap, and surfaced on stderr instead of
  silently extracting zero facts.

Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: ai920wisco <ai920wisco@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:25:48 -07:00

159 lines
6.3 KiB
TypeScript

/**
* T8 — `gbrain config set` strict unknown-key rejection + --force + Levenshtein.
*
* These tests probe the pure helpers (KNOWN_CONFIG_KEYS list, prefix list,
* Levenshtein suggestion against the list). The full `runConfig` CLI
* integration that calls `engine.setConfig` is exercised E2E in T12.
*/
import { describe, test, expect } from 'bun:test';
import { KNOWN_CONFIG_KEYS, KNOWN_CONFIG_KEY_PREFIXES } from '../src/core/config.ts';
import { suggestNearest } from '../src/core/levenshtein.ts';
describe('KNOWN_CONFIG_KEYS', () => {
test('contains the canonical embedding keys', () => {
expect(KNOWN_CONFIG_KEYS).toContain('embedding_model');
expect(KNOWN_CONFIG_KEYS).toContain('embedding_dimensions');
expect(KNOWN_CONFIG_KEYS).toContain('embedding_disabled'); // v0.37 D9
expect(KNOWN_CONFIG_KEYS).toContain('expansion_model');
expect(KNOWN_CONFIG_KEYS).toContain('chat_model');
expect(KNOWN_CONFIG_KEYS).toContain('provider_chat_options');
});
test('contains the search-mode keys (v0.32.3)', () => {
expect(KNOWN_CONFIG_KEYS).toContain('search.mode');
expect(KNOWN_CONFIG_KEYS).toContain('search.cache.enabled');
});
test('contains the models-tier keys (v0.31.12)', () => {
expect(KNOWN_CONFIG_KEYS).toContain('models.default');
expect(KNOWN_CONFIG_KEYS).toContain('models.tier.subagent');
});
test('contains the dream synthesize timeout keys (#1594)', () => {
expect(KNOWN_CONFIG_KEYS).toContain('dream.synthesize.subagent_timeout_ms');
expect(KNOWN_CONFIG_KEYS).toContain('dream.synthesize.subagent_wait_timeout_ms');
});
test('contains the spend-control keys (v0.42.42.0, #2139) — no --force archaeology', () => {
expect(KNOWN_CONFIG_KEYS).toContain('spend.posture');
expect(KNOWN_CONFIG_KEYS).toContain('sync.cost_gate_min_usd');
expect(KNOWN_CONFIG_KEYS).toContain('sync.federated_v2');
expect(KNOWN_CONFIG_KEYS).toContain('embed.backfill_cooldown_min');
expect(KNOWN_CONFIG_KEYS).toContain('embed.backfill_max_usd_per_source_24h');
expect(KNOWN_CONFIG_KEYS).toContain('embed.backfill_max_usd');
});
test('includes the gateway-loop toggle and provider API keys the wave wires', () => {
// The subagent handler's error message tells users to run
// `gbrain config set agent.use_gateway_loop true`; it must be a known key
// or `config set` rejects the wave's own enable command without --force.
expect(KNOWN_CONFIG_KEYS).toContain('agent.use_gateway_loop');
expect(KNOWN_CONFIG_KEYS).toContain('openrouter_api_key');
expect(KNOWN_CONFIG_KEYS).toContain('zeroentropy_api_key');
});
test('no duplicate entries', () => {
const set = new Set(KNOWN_CONFIG_KEYS);
expect(set.size).toBe(KNOWN_CONFIG_KEYS.length);
});
});
describe('KNOWN_CONFIG_KEY_PREFIXES', () => {
test('includes the well-known prefixes', () => {
expect(KNOWN_CONFIG_KEY_PREFIXES).toContain('search.');
expect(KNOWN_CONFIG_KEY_PREFIXES).toContain('models.');
expect(KNOWN_CONFIG_KEY_PREFIXES).toContain('dream.');
expect(KNOWN_CONFIG_KEY_PREFIXES).toContain('provider_chat_options.');
});
test('prefixes end in `.` (consistent shape)', () => {
for (const p of KNOWN_CONFIG_KEY_PREFIXES) {
expect(p).toMatch(/\.$/);
}
});
});
describe('Levenshtein suggestion against KNOWN_CONFIG_KEYS', () => {
test('bug-reporter case: embedding.model → embedding_model', () => {
const got = suggestNearest('embedding.model', KNOWN_CONFIG_KEYS, 3);
expect(got).toBe('embedding_model');
});
test('bug-reporter case: embedding.dimensions → embedding_dimensions', () => {
const got = suggestNearest('embedding.dimensions', KNOWN_CONFIG_KEYS, 3);
expect(got).toBe('embedding_dimensions');
});
test('bug-reporter case: embedding.provider has no perfect match', () => {
// `embedding.provider` is 6+ edits from any canonical key. Either no
// suggestion (returns null) OR suggests a close-ish key like
// `embedding_model`. Either outcome means the user sees a clear
// "unknown key" message + must pick a different name.
const got = suggestNearest('embedding.provider', KNOWN_CONFIG_KEYS, 3);
// The exact mapping depends on Levenshtein bucket; we just verify it
// doesn't accidentally suggest something completely unrelated.
if (got !== null) {
expect(got).toMatch(/^embedding/);
}
});
test('typo: chat_modle → chat_model', () => {
const got = suggestNearest('chat_modle', KNOWN_CONFIG_KEYS, 3);
expect(got).toBe('chat_model');
});
test('typo: search.modes → search.mode', () => {
const got = suggestNearest('search.modes', KNOWN_CONFIG_KEYS, 3);
expect(got).toBe('search.mode');
});
test('completely-unrelated key returns null', () => {
const got = suggestNearest('xyzzy_quux_blah_unrelated', KNOWN_CONFIG_KEYS, 3);
expect(got).toBeNull();
});
test('exact match returns identity (no suggestion noise)', () => {
const got = suggestNearest('embedding_model', KNOWN_CONFIG_KEYS, 3);
expect(got).toBe('embedding_model');
});
});
describe('prefix vs known-key gate logic (mirrored from runConfig)', () => {
// Replicate the gate logic the CLI uses to validate test coverage of
// the decision tree.
function gate(key: string): 'known' | 'prefix' | 'unknown' {
if (KNOWN_CONFIG_KEYS.includes(key)) return 'known';
if (KNOWN_CONFIG_KEY_PREFIXES.some(p => key.startsWith(p))) return 'prefix';
return 'unknown';
}
test('explicit known key → "known"', () => {
expect(gate('embedding_model')).toBe('known');
});
test('search.foo.bar (under prefix) → "prefix"', () => {
expect(gate('search.foo.bar')).toBe('prefix');
});
test('models.custom.x (under prefix) → "prefix"', () => {
expect(gate('models.custom.x')).toBe('prefix');
});
test('provider_chat_options.anthropic (under prefix) → "prefix"', () => {
expect(gate('provider_chat_options.anthropic')).toBe('prefix');
});
test('bug-reporter: embedding.provider → "unknown" (no prefix match)', () => {
expect(gate('embedding.provider')).toBe('unknown');
});
test('bug-reporter: embedding.model → "unknown"', () => {
expect(gate('embedding.model')).toBe('unknown');
});
test('bug-reporter: embedding.dimensions → "unknown"', () => {
expect(gate('embedding.dimensions')).toBe('unknown');
});
});