mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
+5








86962b242b
Collector branch superseding the gateway tool-loop duplicate cluster and adjacent provider fixes. Re-implemented from the best of each PR (deduped by content, not file-overlap); every fix carries test coverage. Gateway tool-loop resume (supersedes #1934 #2062 #2065 #2112 #2274 #2487 #2336 #2257 #2499 #2491, test #2063): - toolLoop now persists the tool-result user turn per round (onToolResultTurn), so a resumed subagent job reloads a balanced transcript instead of dangling assistant tool-calls that non-Anthropic providers reject with AI_MissingToolResultsError. - runSubagentViaGateway reconciles an already-corrupted transcript on resume: it heals every dangling assistant tool-call turn (not just the tail) from the settled subagent_tool_executions rows, re-dispatching idempotent-pending tools and throwing on non-idempotent, mirroring the legacy Anthropic path. Terminal early-return for a transcript that already reached end_turn. - repairToolPairing() is a last-resort normalization at the chat() boundary (from #2336): back-fills error stubs for any assistant tool-call still unanswered (partial turns, provider-duplicated/dropped IDs on local models, length-truncated batches). No-op on balanced input. - toModelMessages is Date-safe (Postgres timestamptz -> ISO via a JSON round trip at the SDK boundary, never a ::jsonb cast; degrades bigint/circular to a string instead of throwing) and drops non-string text blocks reasoning models emit that AI SDK v6 rejects (#2488). Adjacent provider fixes: - Model-aware default max output tokens: thinking-by-default Claude 5 models get headroom (gateway 32000, think 16000) while everything else stays 4096/4000, so DeepSeek/OpenAI subagents don't exceed provider caps (#2614 #2806). - DeepSeek: promote reasoning_content into content when content is empty, via a fail-open recipe fetch shim (#2617). - OpenRouter: map openrouter_api_key (config + env) into OPENROUTER_API_KEY through buildGatewayConfig; register agent.use_gateway_loop, zeroentropy and openrouter keys in KNOWN_CONFIG_KEYS so `config set` accepts them (#2572, config key from #2112). Preserves JSONB (no JSON.stringify into ::jsonb), engine parity, source isolation, and trust-boundary invariants. Verified with the gbrain-pr-test-env consumer matrix (clawlancer/Postgres, gstack + hivemindos/PGLite) baseline-FAIL -> candidate-PASS on the same repro, plus bun run verify (31/31) and 439 targeted unit + e2e tests. Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: thomaskong119 <thomaskong119@hotmail.com> Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com> Co-authored-by: brettdavies <brettdavies@users.noreply.github.com> Co-authored-by: ivandebot <187176982+ivandebot@users.noreply.github.com> Co-authored-by: Rafael Reis <rafael.reis@contabilizei.com.br> Co-authored-by: fbal23 <fbal.public@gmail.com> Co-authored-by: javieraldape <javieraldape@users.noreply.github.com> Co-authored-by: David Carolan <david@joyrestart.com> Co-authored-by: Masashi-Ono0611 <masashi.ono.0611@gmail.com> Co-authored-by: spiky02plateau <155588579+spiky02plateau@users.noreply.github.com> Co-authored-by: psam-717 <mphilannorbah@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
148 lines
5.7 KiB
TypeScript
148 lines
5.7 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');
|
|
});
|
|
|
|
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 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.');
|
|
});
|
|
|
|
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('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');
|
|
});
|
|
});
|