mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(core): shared computeSyncDelta + spend-posture module (#2139) sync-delta.ts: ONE implementation of "what changed since last_commit", consumed by both the sync executor and the inline cost estimator so the gate's dollar figure can't drift from what the sync imports. spend-posture.ts: spend.posture config + parseUsdLimit/formatUsdLimit off-switch parsing (off/unlimited/none → Infinity; undefined at the budget boundary so ledger rows never serialize null). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): delta-aware cost estimator + non-TTY auto-defer + per-source failure acks (#2139) The inline-embed cost gate was a ~400x phantom: it priced the entire tree whenever the working tree was dirty (always, on an active brain), then blocked the daily cron with exit 2. Now: - performSyncInner + the estimator both route through computeSyncDelta, so the estimate mirrors execution (fetch-first delta; dirty-but-caught-up tree → $0). - shouldBlockSync is posture-aware; non-TTY above floor AUTO-DEFERS embeds to capped backfill jobs (exit 0) instead of wedging — single shared runInlineCostGate on both --all and single-source paths. - --full prices delta + stale backlog (full sync sweeps it inline). - off/unlimited on the cost knobs; tokenmax bypasses the backfill cap (still ledgered) but never the cooldown. - --skip-failed/--retry-failed scoped per source; the D15 parallel refusal is lifted (the #1939 ledger is per-source + lock-serialized). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(config): register spend-control keys + validate spend.posture (#2139) Adds spend.posture + the five previously --force-only spend knobs to KNOWN_CONFIG_KEYS so `config set` accepts them directly (removes the archaeology the issue complained about), and rejects invalid spend.posture values at set time with a paste-ready hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reindex,enrich,onboard): spend.posture across the remaining cost gates (#2139) reindex-code: tokenmax makes the cost gate informational; --max-cost accepts off/unlimited. enrich + onboard --auto: tokenmax lifts the refuse-without-cap guardrail and runs UNCAPPED (spend still ledgered by BudgetTracker). Explicit --max-usd always wins over posture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: cost-gate, delta estimator, spend-posture, off-switch coverage (#2139) New sync-delta + sync-cost-estimate unit suites; rewritten cost-gate serial tests (auto-defer instead of exit 2, posture, off-switch, format split, single-source); parseUsdLimit/posture-aware shouldBlockSync; backfill cap-off + tokenmax-bypass + cooldown-still-refuses; config known-key acceptance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(spend-controls): single spend-control surface + ref-map + follow-up TODOs (#2139) New docs/operations/spend-controls.md (every gate, key, default, off switch, posture interaction); CLAUDE.md reference-map row; two P3 follow-up TODOs (measured chunk-count gating, per-source defer granularity). llms bundles regenerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(spend): SSRF-harden estimator fetch + complete off/uncapped across reindex/enrich/onboard (#2139) Ship-stage codex pre-landing review caught four P1s in the secondary cost gates: - The delta estimator's fetch-first ran `git fetch` through the plain git() helper, bypassing the GIT_SSRF_FLAGS + GIT_TERMINAL_PROMPT=0 hardening that real sync uses. Added `fetchRemote()` to git-remote.ts (same flags as pullRepo) and route the estimator through it — a cost preview / dry-run can no longer hit a remote through a less-protected path. - `reindex --max-cost off`, `enrich --max-usd off`, `onboard --auto --max-usd off` were parsed but didn't actually proceed/uncap. Now: explicit off (and spend.posture=tokenmax) proceed past the confirmation/missing-cap refusal AND run uncapped. enrich threads an Infinity sentinel mapped to "no BudgetTracker ceiling" (never raw Infinity → no null in audit rows); reindex/onboard use their native undefined=uncapped path. Spend still ledgered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.42.45.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(KEY_FILES): update sync/embedding/git-remote/reindex entries to post-#2139 truth document-release pass: the cost-gate entries described the pre-#2139 behavior (full-tree-ceiling estimator, --skip-failed-rejects-under-parallel, exit-2 confirmation gate). Updated to current truth — delta-aware estimator via the shared computeSyncDelta, per-source failure acks under parallel, non-TTY auto-defer (no exit 2), posture-aware shouldBlockSync. Added entries for the two new core modules (sync-delta.ts, spend-posture.ts) + fetchRemote on git-remote.ts + reindex --max-cost off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.2 KiB
TypeScript
139 lines
5.2 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('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');
|
|
});
|
|
});
|