Files
gbrain/test/sync-cost-estimate.test.ts
T
5c49225e4b v0.42.45.0 feat(sync): delta-aware cost estimator — stop wedging the daily cron (#2139) (#2224)
* 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>
2026-06-16 15:08:47 -07:00

141 lines
6.1 KiB
TypeScript

/**
* v0.42.42.0 (#2139) — estimateInlineNewTokens ladder coverage.
*
* The inline cost estimator now MIRRORS EXECUTION (delta, not full-tree
* ceiling) so the gate's dollar figure stops being a ~400x phantom on a busy
* brain. Real temp git repos, no PGLite. estimateInlineNewTokens is exported
* from commands/sync.ts; CHUNKER_VERSION is the live chunker version.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
import { tmpdir } from 'os';
import { join } from 'path';
import { estimateInlineNewTokens } from '../src/commands/sync.ts';
import { CHUNKER_VERSION } from '../src/core/chunkers/code.ts';
const CURRENT = String(CHUNKER_VERSION);
let repo: string;
function commitAll(msg: string): string {
execSync('git add -A', { cwd: repo, stdio: 'pipe' });
execSync(`git commit -m "${msg}"`, { cwd: repo, stdio: 'pipe' });
return execSync('git rev-parse HEAD', { cwd: repo, stdio: 'pipe' }).toString().trim();
}
function src(over: Partial<{ last_commit: string | null; chunker_version: string | null; config: Record<string, unknown> }> = {}) {
return {
local_path: repo,
config: over.config ?? {},
last_commit: over.last_commit ?? null,
chunker_version: over.chunker_version ?? null,
};
}
beforeEach(() => {
repo = mkdtempSync(join(tmpdir(), 'gbrain-est-'));
execSync('git init', { cwd: repo, stdio: 'pipe' });
execSync('git config user.email "t@t.com"', { cwd: repo, stdio: 'pipe' });
execSync('git config user.name "T"', { cwd: repo, stdio: 'pipe' });
mkdirSync(join(repo, 'topics'), { recursive: true });
});
afterEach(() => {
if (repo) rmSync(repo, { recursive: true, force: true });
});
describe('estimateInlineNewTokens — ladder', () => {
test('chunker drift → full-tree ceiling even with an empty git delta', () => {
writeFileSync(join(repo, 'topics/a.md'), 'some body content here');
const head = commitAll('base');
// git unchanged (last_commit == HEAD) but chunker drifted → must NOT be 0.
const r = estimateInlineNewTokens([src({ last_commit: head, chunker_version: 'STALE-0' })], CURRENT);
expect(r.tokens).toBeGreaterThan(0);
expect(r.estimateKind).toBe('ceiling');
expect(r.ceilingReasons).toContain('chunker_drift');
expect(r.changedSources).toBe(1);
});
test('[D2A headline] HEAD==last_commit + current chunker + DIRTY tree → 0 (unchanged)', () => {
writeFileSync(join(repo, 'topics/a.md'), 'body');
const head = commitAll('base');
// Dirty the tree (untracked scratch + uncommitted edit) — attached sync
// imports nothing, so the estimate must be 0. This is the exact pre-fix
// false-fire shape.
writeFileSync(join(repo, 'scratch.tmp'), 'agent scratch');
writeFileSync(join(repo, 'topics/a.md'), 'uncommitted edit');
const r = estimateInlineNewTokens([src({ last_commit: head, chunker_version: CURRENT })], CURRENT);
expect(r.tokens).toBe(0);
expect(r.estimateKind).toBe('unchanged');
expect(r.unchangedSources).toBe(1);
});
test('first sync (last_commit null) → full-tree ceiling', () => {
writeFileSync(join(repo, 'topics/a.md'), 'body content');
commitAll('base');
const r = estimateInlineNewTokens([src({ last_commit: null, chunker_version: CURRENT })], CURRENT);
expect(r.tokens).toBeGreaterThan(0);
expect(r.estimateKind).toBe('ceiling');
expect(r.ceilingReasons).toContain('first_sync');
});
test('delta rung: only changed committed files priced; deletes cost 0', () => {
writeFileSync(join(repo, 'topics/a.md'), 'a'.repeat(400));
writeFileSync(join(repo, 'topics/b.md'), 'b'.repeat(400));
const base = commitAll('base');
writeFileSync(join(repo, 'topics/a.md'), 'a'.repeat(800)); // modify
rmSync(join(repo, 'topics/b.md')); // delete → 0
commitAll('change');
const r = estimateInlineNewTokens([src({ last_commit: base, chunker_version: CURRENT })], CURRENT);
expect(r.estimateKind).toBe('delta');
expect(r.tokens).toBeGreaterThan(0); // a.md priced
// A pure-delete delta would be 0; here a.md modify keeps it > 0. Sanity:
// the magnitude is delta-scale (one ~800-char file), not full-tree.
});
test('delta rung: non-syncable changed file is filtered out (markdown strategy)', () => {
writeFileSync(join(repo, 'topics/a.md'), 'md');
const base = commitAll('base');
writeFileSync(join(repo, 'notes.txt'), 'x'.repeat(4000)); // .txt under markdown strategy → not syncable
commitAll('add txt');
const r = estimateInlineNewTokens([src({ last_commit: base, chunker_version: CURRENT })], CURRENT);
// No syncable changes → delta priced at 0 tokens (but the source changed).
expect(r.tokens).toBe(0);
expect(r.estimateKind).toBe('delta');
});
test('syncEnabled:false sources are skipped (neither changed nor unchanged)', () => {
writeFileSync(join(repo, 'topics/a.md'), 'body');
commitAll('base');
const r = estimateInlineNewTokens([src({ last_commit: null, config: { syncEnabled: false } })], CURRENT);
expect(r.tokens).toBe(0);
expect(r.changedSources).toBe(0);
expect(r.unchangedSources).toBe(0);
});
test('mixed: one ceiling source + one unchanged source → estimateKind mixed-or-ceiling, reasons captured', () => {
writeFileSync(join(repo, 'topics/a.md'), 'body');
const head = commitAll('base');
const r = estimateInlineNewTokens(
[
src({ last_commit: null, chunker_version: CURRENT }), // first_sync ceiling
src({ last_commit: head, chunker_version: CURRENT }), // unchanged
],
CURRENT,
);
expect(r.ceilingReasons).toContain('first_sync');
expect(r.unchangedSources).toBe(1);
// hadCeiling true, hadDelta false → 'ceiling' aggregate.
expect(r.estimateKind).toBe('ceiling');
});
test('missing local_path source contributes nothing', () => {
const r = estimateInlineNewTokens(
[{ local_path: null, config: {}, last_commit: null, chunker_version: CURRENT }],
CURRENT,
);
expect(r.tokens).toBe(0);
expect(r.changedSources).toBe(0);
});
});