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>
223 lines
8.4 KiB
TypeScript
223 lines
8.4 KiB
TypeScript
/**
|
|
* Tests for src/core/embed-backfill-submit.ts (v0.40 D19).
|
|
*
|
|
* Validates the submission gate layer:
|
|
* - Default path: submits with priority 5 + idempotency bucket
|
|
* - Cooldown: refuses re-submission inside the window
|
|
* - Active-job: refuses while a same-source job is active/waiting
|
|
* - 24h spend cap: refuses when accumulated spend >= cap
|
|
* - Config overrides honored (per-test cap + cooldown)
|
|
* - Override knobs in opts honored (test seam)
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import {
|
|
submitEmbedBackfill,
|
|
COOLDOWN_CONFIG_KEY,
|
|
SPEND_CAP_CONFIG_KEY,
|
|
} from '../src/core/embed-backfill-submit.ts';
|
|
import { MinionQueue } from '../src/core/minions/queue.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
}, 30000); // 30s — PGLite WASM cold-start + 89 migrations exceeds 5s default
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Surgical reset (mirrors test/minions.test.ts) — full TRUNCATE wipes the
|
|
// config table's `version` key that MinionQueue.ensureSchema() reads.
|
|
await engine.executeRaw('DELETE FROM minion_jobs');
|
|
});
|
|
|
|
describe('submitEmbedBackfill — happy path', () => {
|
|
test('submits with priority 5 + idempotency key on a clean source', async () => {
|
|
const result = await submitEmbedBackfill(engine, 'default', { reason: 'unit' });
|
|
expect(result.status).toBe('submitted');
|
|
expect(result.jobId).toBeDefined();
|
|
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.getJob(result.jobId!);
|
|
expect(job).not.toBeNull();
|
|
expect(job!.name).toBe('embed-backfill');
|
|
expect(job!.priority).toBe(5);
|
|
expect((job!.data as { sourceId: string }).sourceId).toBe('default');
|
|
});
|
|
|
|
test('respects opts.priority override', async () => {
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
priority: -10,
|
|
});
|
|
expect(result.status).toBe('submitted');
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.getJob(result.jobId!);
|
|
expect(job!.priority).toBe(-10);
|
|
});
|
|
});
|
|
|
|
describe('submitEmbedBackfill — cooldown gate', () => {
|
|
test('blocks re-submission while a same-source job is active', async () => {
|
|
const queue = new MinionQueue(engine);
|
|
// Seed an active job manually
|
|
await queue.add('embed-backfill', { sourceId: 'default' }, {});
|
|
await engine.executeRaw(
|
|
`UPDATE minion_jobs SET status='active' WHERE name='embed-backfill'`,
|
|
);
|
|
|
|
const result = await submitEmbedBackfill(engine, 'default', { reason: 'unit' });
|
|
expect(result.status).toBe('cooldown');
|
|
expect(result.cooldownRemainingSeconds).toBeUndefined();
|
|
});
|
|
|
|
test('blocks re-submission inside the cooldown window after recent finish', async () => {
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.add('embed-backfill', { sourceId: 'default' }, {});
|
|
// Mark completed 1 minute ago
|
|
await engine.executeRaw(
|
|
`UPDATE minion_jobs SET status='completed', finished_at=NOW() - INTERVAL '1 minute' WHERE id=$1`,
|
|
[job.id],
|
|
);
|
|
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
cooldownMinOverride: 10, // 10min cooldown; 1min elapsed → blocked
|
|
});
|
|
expect(result.status).toBe('cooldown');
|
|
expect(result.cooldownRemainingSeconds).toBeGreaterThan(0);
|
|
expect(result.cooldownRemainingSeconds).toBeLessThanOrEqual(10 * 60);
|
|
});
|
|
|
|
test('allows re-submission after cooldown elapses', async () => {
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.add('embed-backfill', { sourceId: 'default' }, {});
|
|
// Mark completed 11 minutes ago — past the 10-min cooldown
|
|
await engine.executeRaw(
|
|
`UPDATE minion_jobs SET status='completed', finished_at=NOW() - INTERVAL '11 minutes' WHERE id=$1`,
|
|
[job.id],
|
|
);
|
|
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
cooldownMinOverride: 10,
|
|
});
|
|
expect(result.status).toBe('submitted');
|
|
});
|
|
|
|
test('config-overridable cooldown (via embed.backfill_cooldown_min)', async () => {
|
|
await engine.setConfig(COOLDOWN_CONFIG_KEY, '60'); // 60min cooldown
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.add('embed-backfill', { sourceId: 'default' }, {});
|
|
await engine.executeRaw(
|
|
`UPDATE minion_jobs SET status='completed', finished_at=NOW() - INTERVAL '30 minutes' WHERE id=$1`,
|
|
[job.id],
|
|
);
|
|
|
|
const result = await submitEmbedBackfill(engine, 'default', { reason: 'unit' });
|
|
expect(result.status).toBe('cooldown');
|
|
});
|
|
});
|
|
|
|
describe('submitEmbedBackfill — 24h spend cap', () => {
|
|
test('refuses when spend24hFn returns >= cap', async () => {
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
spendCapUsdOverride: 25,
|
|
spend24hFn: async () => 25,
|
|
});
|
|
expect(result.status).toBe('spend_capped');
|
|
expect(result.spend24hUsd).toBe(25);
|
|
expect(result.spendCapUsd).toBe(25);
|
|
});
|
|
|
|
test('admits when spend24hFn returns < cap', async () => {
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
spendCapUsdOverride: 25,
|
|
spend24hFn: async () => 24.99,
|
|
});
|
|
expect(result.status).toBe('submitted');
|
|
});
|
|
|
|
test('config-overridable spend cap (via embed.backfill_max_usd_per_source_24h)', async () => {
|
|
await engine.setConfig(SPEND_CAP_CONFIG_KEY, '5');
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
spend24hFn: async () => 5,
|
|
});
|
|
expect(result.status).toBe('spend_capped');
|
|
expect(result.spendCapUsd).toBe(5);
|
|
});
|
|
|
|
// v0.42.42.0 (#2139): off-switch + tokenmax bypass.
|
|
test('cap "off" → submits even at huge spend (Infinity cap never tripped)', async () => {
|
|
await engine.setConfig(SPEND_CAP_CONFIG_KEY, 'off');
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
spend24hFn: async () => 1e9,
|
|
});
|
|
expect(result.status).toBe('submitted');
|
|
});
|
|
|
|
test('0 falls back to the default cap (off semantics ≠ 0)', async () => {
|
|
await engine.setConfig(SPEND_CAP_CONFIG_KEY, '0');
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
spend24hFn: async () => 25, // == default $25 → capped
|
|
});
|
|
expect(result.status).toBe('spend_capped');
|
|
expect(result.spendCapUsd).toBe(25);
|
|
});
|
|
|
|
test('spend.posture=tokenmax bypasses the cap, marks spendCapBypassed', async () => {
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
postureOverride: 'tokenmax',
|
|
spendCapUsdOverride: 25,
|
|
spend24hFn: async () => 100, // way over cap
|
|
});
|
|
expect(result.status).toBe('submitted');
|
|
expect(result.spendCapBypassed).toBe(true);
|
|
expect(result.spend24hUsd).toBe(100);
|
|
});
|
|
|
|
test('tokenmax does NOT bypass the cooldown (axis split — churn protection stays)', async () => {
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.add('embed-backfill', { sourceId: 'default' }, {});
|
|
await engine.executeRaw(
|
|
`UPDATE minion_jobs SET status='completed', finished_at=NOW() - INTERVAL '1 minute' WHERE id=$1`,
|
|
[job.id],
|
|
);
|
|
const result = await submitEmbedBackfill(engine, 'default', {
|
|
reason: 'unit',
|
|
postureOverride: 'tokenmax',
|
|
cooldownMinOverride: 10,
|
|
spend24hFn: async () => 1e9,
|
|
});
|
|
expect(result.status).toBe('cooldown'); // posture lifts the cap, NOT the cooldown
|
|
});
|
|
});
|
|
|
|
describe('submitEmbedBackfill — source isolation', () => {
|
|
test('cooldown is per-source, not global', async () => {
|
|
await engine.executeRaw(
|
|
`INSERT INTO sources (id, name, config) VALUES ('other', 'other', '{"federated":true}') ON CONFLICT (id) DO NOTHING`,
|
|
);
|
|
const queue = new MinionQueue(engine);
|
|
// Active job on 'default'
|
|
await queue.add('embed-backfill', { sourceId: 'default' }, {});
|
|
await engine.executeRaw(`UPDATE minion_jobs SET status='active' WHERE name='embed-backfill'`);
|
|
|
|
// Submit for 'other' — should NOT be blocked
|
|
const result = await submitEmbedBackfill(engine, 'other', { reason: 'unit' });
|
|
expect(result.status).toBe('submitted');
|
|
});
|
|
});
|