v0.40.5.0 Federated Sync v2 — parallel source sync + push triggers + per-source health (#1322)

* wip: federated sync v2 pre-merge snapshot

* v0.40.5.0 Federated Sync v2 — parallel source sync + push triggers + per-source health

Bump VERSION + package.json + CHANGELOG header + migration walkthrough filename
to v0.40.5.0 (claiming the next free slot in the v0.40.x patch series after
master's v0.40.1.0).

What ships (6 components, all behind sync.federated_v2 feature flag default-on):
1. Per-source sync lock — syncLockId(sourceId), phantom-redirect parity
2. Parallel sync --all — pMapAllSettled fan-out, --max-sources N cap
3. embed-backfill minion handler — D2 per-source lock + D6 $10/job budget + D15.1
   fire-and-forget submission + D19 source-level cooldown + 24h $25 rolling cap
4. sync trigger CLI + POST /webhooks/github — HMAC-verified (60 req/min/IP),
   X-GitHub-Event=push + ref filter against tracked_branch
5. sources status + federation_health doctor — batched GROUP BY pipeline
   (4 queries instead of 6×N per-source roundtrips)
6. sources federate/unfederate hook — auto-submit embed-backfill on flip

Correctness fixes (unconditional):
- D21: sync.ts:959 facts backstop now passes sourceId to engine.getPage
- D15.4: redactSourceConfig + CI guard prevent webhook_secret leak
- D15.5: safeHexEqual extracted to src/core/timing-safe.ts

Schema:
- Migration v89 (sources_github_repo_index): partial expression index on
  config->>'github_repo' for fast webhook source-lookup

Tests:
- 14 new test files, 112 cases. 4 IRON-RULE regressions pinned (SYNC_LOCK_ID
  back-compat, phantom per-source lock, embed-backfill kill+resume,
  webhook HMAC prefix-strip). All 9449 unit tests pass.

Caught at test-write time: the webhook handler had a Buffer.from('sha256=...',
'hex') truncation bug — without the prefix-strip, every signature would have
"matched" empty buffers. Pinned by a test/sources-webhook.test.ts IRON-RULE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(check-source-config-leak): tighten regex to source-row patterns only

The v0.40.5.0 wave added scripts/check-source-config-leak.sh with a
too-broad pattern (JSON\.stringify\(.*config) that flagged any variable
named 'config' — catching the GLOBAL gbrain config.json serializers in
src/commands/init.ts (status envelopes) and src/core/config.ts (the
config-file write site). On the CI runner without rg installed, the
grep -rE fallback fired correctly and produced 4 false positives that
broke the `verify` script.

Tightened the patterns to specifically match `(source|src|row|s).config`
property access — the actual risk shape (a sources-table row being
serialized whole). The global gbrain config has a different shape and
threat model (file-mode 0o600 at the write site), so it's safe to
exempt at the regex level rather than per-file whitelist.

Also fixed a latent bug: the rg branch used `--include='*.ts'` (grep's
flag, not rg's). rg silently rejected it and CANDIDATES came back empty,
so the local-dev runs (which have rg) would never have caught a real
leak. Now branches on tool availability: `-g '*.ts'` for rg, `--include`
for grep -rE. Both branches verified against a synthetic leak fixture.

Also added init.ts + config.ts to the whitelist as a belt-and-suspenders
since they handle gbrain-global config (not source rows) and could
otherwise reflect-back via regex iteration.

CI: `bun run verify` exit 0 locally with both the original false-positive
fixture (clean repo) and a synthetic leak fixture (correctly caught,
exit 1).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-23 10:21:59 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent d28be5d091
commit df86ea5f1d
40 changed files with 3968 additions and 65 deletions
+86
View File
@@ -0,0 +1,86 @@
/**
* v0.40 Federated Sync v2 — per-source lock contract.
*
* Pins the iron-rule regression for the SYNC_LOCK_ID rename:
* - SYNC_LOCK_ID === syncLockId('default') (back-compat alias)
* - syncLockId(X) !== syncLockId(Y) for X !== Y (per-source isolation)
* - two concurrent tryAcquireDbLock calls against DIFFERENT sources both succeed
* - two concurrent tryAcquireDbLock calls against the SAME source: second returns null
*
* Without this guard, a future drift in the constant value would silently change
* semantics (e.g. someone hardcoding 'gbrain-sync' elsewhere would no longer match
* the same row, breaking the writer-window exclusion that performSync relies on).
*/
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 { syncLockId, SYNC_LOCK_ID, tryAcquireDbLock } from '../src/core/db-lock.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
describe('syncLockId (per-source lock helper)', () => {
test('default source returns gbrain-sync:default', () => {
expect(syncLockId('default')).toBe('gbrain-sync:default');
});
test('non-default source returns gbrain-sync:<id>', () => {
expect(syncLockId('zion-brain')).toBe('gbrain-sync:zion-brain');
expect(syncLockId('media-corpus')).toBe('gbrain-sync:media-corpus');
});
test('IRON-RULE: SYNC_LOCK_ID back-compat alias resolves to syncLockId(default)', () => {
expect(SYNC_LOCK_ID).toBe(syncLockId('default'));
expect(SYNC_LOCK_ID).toBe('gbrain-sync:default');
});
test('different sources produce distinct lock keys', () => {
expect(syncLockId('a')).not.toBe(syncLockId('b'));
});
});
describe('tryAcquireDbLock with per-source keys', () => {
test('two locks against different sources both succeed', async () => {
const lockA = await tryAcquireDbLock(engine, syncLockId('source-a'));
const lockB = await tryAcquireDbLock(engine, syncLockId('source-b'));
expect(lockA).not.toBeNull();
expect(lockB).not.toBeNull();
await lockA?.release();
await lockB?.release();
});
test('second lock against the same source returns null while first is held', async () => {
const first = await tryAcquireDbLock(engine, syncLockId('source-c'));
expect(first).not.toBeNull();
const second = await tryAcquireDbLock(engine, syncLockId('source-c'));
expect(second).toBeNull();
await first?.release();
// After release, third acquire succeeds.
const third = await tryAcquireDbLock(engine, syncLockId('source-c'));
expect(third).not.toBeNull();
await third?.release();
});
test('SYNC_LOCK_ID and syncLockId(default) acquire the SAME lock row', async () => {
// This is the critical back-compat check: pre-v0.40 callers using SYNC_LOCK_ID
// and new callers using syncLockId('default') MUST conflict (not bypass each other).
const first = await tryAcquireDbLock(engine, SYNC_LOCK_ID);
expect(first).not.toBeNull();
const second = await tryAcquireDbLock(engine, syncLockId('default'));
expect(second).toBeNull();
await first?.release();
});
});
+102
View File
@@ -0,0 +1,102 @@
/**
* Tests for src/commands/doctor.ts:checkFederationHealth (v0.40 T12).
*
* Three-state contract:
* ok — single-source brain, or every federated source healthy
* warn — lag > 1h + federated, coverage < 95% with chunks > 100, OR 3+ failures
* fail — lag > 24h, OR coverage < 50% with chunks > 1000
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { checkFederationHealth } from '../src/commands/doctor.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await engine.executeRaw('DELETE FROM minion_jobs');
await engine.executeRaw('DELETE FROM content_chunks');
await engine.executeRaw('DELETE FROM pages');
await engine.executeRaw(`DELETE FROM sources WHERE id != 'default'`);
});
describe('checkFederationHealth', () => {
test('single-source brain → ok with "no federation to check"', async () => {
const check = await checkFederationHealth(engine);
expect(check.name).toBe('federation_health');
expect(check.status).toBe('ok');
expect(check.message).toContain('no federation to check');
});
test('multi-source healthy brain → ok with source count', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config, last_sync_at) VALUES ('extra', 'extra', '{"federated":true}', NOW())`,
);
const check = await checkFederationHealth(engine);
expect(check.status).toBe('ok');
expect(check.message).toContain('source(s) healthy');
});
test('source with lag > 1h + federated → warn with remediation', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config, last_sync_at) VALUES ('stale-source', 'stale-source', '{"federated":true}', NOW() - INTERVAL '2 hours')`,
);
const check = await checkFederationHealth(engine);
expect(check.status).toBe('warn');
expect(check.message).toContain('stale-source');
expect(check.message).toContain('gbrain sync trigger --source stale-source');
});
test('source with lag > 24h → fail with remediation', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config, last_sync_at) VALUES ('dead-source', 'dead-source', '{"federated":true}', NOW() - INTERVAL '48 hours')`,
);
const check = await checkFederationHealth(engine);
expect(check.status).toBe('fail');
expect(check.message).toContain('dead-source');
expect(check.message).toContain('gbrain sync trigger');
});
test('source with low embed coverage + chunks > 100 → warn', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config, last_sync_at) VALUES ('uncovered', 'uncovered', '{"federated":true}', NOW())`,
);
// Seed 200 pages with chunks; 10 embedded.
for (let i = 0; i < 200; i++) {
await engine.putPage(`p${i}`, { type: 'note', title: `p${i}`, compiled_truth: `body ${i}` }, { sourceId: 'uncovered' });
await engine.upsertChunks(
`p${i}`,
[{
chunk_index: 0,
chunk_text: `chunk ${i}`,
chunk_source: 'compiled_truth',
token_count: 1,
embedding: i < 10 ? new Float32Array(1536) : undefined,
}],
{ sourceId: 'uncovered' },
);
}
const check = await checkFederationHealth(engine);
expect(check.status).toBe('warn');
expect(check.message).toContain('uncovered');
expect(check.message).toContain('embed coverage');
expect(check.message).toContain('gbrain jobs submit embed-backfill');
});
test('synced + zero pages → ok (vacuous truth, no coverage warn)', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config, last_sync_at) VALUES ('empty', 'empty', '{"federated":true}', NOW())`,
);
const check = await checkFederationHealth(engine);
expect(check.status).toBe('ok');
});
});
+8 -4
View File
@@ -20,7 +20,8 @@ import { tmpdir } from 'os';
import { hasDatabase, setupDB, teardownDB, getEngine } from './helpers.ts';
import { withEnv } from '../helpers/with-env.ts';
import { runExtractFacts } from '../../src/core/cycle/extract-facts.ts';
import { tryAcquireDbLock, SYNC_LOCK_ID } from '../../src/core/db-lock.ts';
// v0.40: per-source lock id replaces the legacy bare SYNC_LOCK_ID constant.
// Tests below hand-craft the lock row via SQL to simulate contention.
const SKIP = !hasDatabase();
const describeMaybe = SKIP ? describe.skip : describe;
@@ -41,7 +42,8 @@ beforeEach(async () => {
// Per-test cleanup: truncate the tables this suite mutates.
await engine.executeRaw('TRUNCATE TABLE facts RESTART IDENTITY CASCADE');
await engine.executeRaw('DELETE FROM pages');
await engine.executeRaw(`DELETE FROM gbrain_cycle_locks WHERE id='gbrain-sync'`);
// v0.40: phantom redirect now uses per-source lock id (gbrain-sync:<source>).
await engine.executeRaw(`DELETE FROM gbrain_cycle_locks WHERE id='gbrain-sync:default'`);
});
function tempBrain(): string {
@@ -154,9 +156,10 @@ describeMaybe('phantom-redirect E2E (Postgres)', () => {
// would still see the row as held (different "logical" holder via the
// pid check, but the TTL is what really gates re-acquire). Override
// by inserting a row with a different pid + future TTL.
// v0.40 D16: phantom acquires per-source lock; default source = gbrain-sync:default.
await engine.executeRaw(
`INSERT INTO gbrain_cycle_locks (id, holder_pid, holder_host, acquired_at, ttl_expires_at)
VALUES ('gbrain-sync', 9999, 'simulated-other-host', now(), now() + interval '1 hour')
VALUES ('gbrain-sync:default', 9999, 'simulated-other-host', now(), now() + interval '1 hour')
ON CONFLICT (id) DO UPDATE SET holder_pid=9999, ttl_expires_at=now() + interval '1 hour'`,
);
@@ -180,7 +183,8 @@ describeMaybe('phantom-redirect E2E (Postgres)', () => {
expect(existsSync(join(brainDir, 'alice.md'))).toBe(true);
// Cleanup
await engine.executeRaw(`DELETE FROM gbrain_cycle_locks WHERE id='gbrain-sync'`);
// v0.40: phantom redirect now uses per-source lock id (gbrain-sync:<source>).
await engine.executeRaw(`DELETE FROM gbrain_cycle_locks WHERE id='gbrain-sync:default'`);
} finally {
rmSync(brainDir, { recursive: true, force: true });
}
+174
View File
@@ -0,0 +1,174 @@
/**
* 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);
});
});
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');
});
});
+230
View File
@@ -0,0 +1,230 @@
/**
* Tests for src/core/embed-stale.ts (v0.40 D15.2).
*
* Hermetic — uses an injected `embedFn` so no network call lands. Validates:
* - empty stale set → done:true, embedded:0
* - multi-batch run → embed every stale chunk, advance cursor correctly
* - kill mid-flight (signal.aborted) → aborted:true, partial progress preserved
* - resume from cursor → picks up where prior call left off (DB predicate)
* - per-page embedFn throw → logged + skipped, NOT propagated; chunks stay NULL
*
* Why PGLite: validates the engine.listStaleChunks/getChunks/upsertChunks
* roundtrip the helper depends on, not just the loop control flow.
*/
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 { embedStaleForSource } from '../src/core/embed-stale.ts';
import type { ChunkInput } from '../src/core/types.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
/** Seed a page with N stale chunks (no embedding) into the default source. */
async function seedPageWithStaleChunks(slug: string, chunkCount: number): Promise<void> {
await engine.putPage(slug, {
type: 'note',
title: slug,
compiled_truth: `# ${slug}\n\nseeded`,
});
const chunks: ChunkInput[] = Array.from({ length: chunkCount }, (_, i) => ({
chunk_index: i,
chunk_text: `chunk ${i} of ${slug}`,
chunk_source: 'compiled_truth',
token_count: 4,
embedding: undefined, // NULL = stale
}));
await engine.upsertChunks(slug, chunks);
}
/** Deterministic fake embedder — returns unit-length 1536-dim vectors with
* first dim = text length, so we can assert specific chunks got embedded. */
function fakeEmbedFn(texts: string[]): Promise<Float32Array[]> {
return Promise.resolve(
texts.map((t) => {
const v = new Float32Array(1536);
v[0] = t.length;
v[1] = 1;
return v;
}),
);
}
describe('embedStaleForSource', () => {
test('empty stale set returns done:true with zero embedded', async () => {
const result = await embedStaleForSource(engine, 'default', {
embedFn: fakeEmbedFn,
});
expect(result).toEqual({
embedded: 0,
chunksProcessed: 0,
pagesProcessed: 0,
lastCursor: null,
done: true,
aborted: false,
});
});
test('embeds every stale chunk across multiple pages in one call', async () => {
await seedPageWithStaleChunks('a', 5);
await seedPageWithStaleChunks('b', 3);
const result = await embedStaleForSource(engine, 'default', {
embedFn: fakeEmbedFn,
});
expect(result.done).toBe(true);
expect(result.aborted).toBe(false);
expect(result.embedded).toBe(8);
expect(result.pagesProcessed).toBe(2);
// Verify DB: zero stale remaining for default.
const stale = await engine.countStaleChunks({ sourceId: 'default' });
expect(stale).toBe(0);
});
test('respects batchSize for cursor pagination', async () => {
await seedPageWithStaleChunks('a', 3);
await seedPageWithStaleChunks('b', 3);
let batchCount = 0;
const result = await embedStaleForSource(engine, 'default', {
embedFn: fakeEmbedFn,
batchSize: 2,
onProgress: () => {
batchCount++;
},
});
expect(result.embedded).toBe(6);
// 2-chunk batches across 6 stale rows = at least 3 progress callbacks.
expect(batchCount).toBeGreaterThanOrEqual(3);
});
test('IRON-RULE: aborted mid-flight → aborted:true, partial progress preserved', async () => {
await seedPageWithStaleChunks('a', 4);
await seedPageWithStaleChunks('b', 4);
await seedPageWithStaleChunks('c', 4);
const controller = new AbortController();
// Batch size 4 = one page per batch. concurrency 1 = serialize keys.
// Abort fires inside embedFn for page 'b', so 'a' lands, 'b' aborts mid-call,
// and the third batch ('c') never starts.
const result = await embedStaleForSource(engine, 'default', {
batchSize: 4,
concurrency: 1,
signal: controller.signal,
embedFn: async (texts) => {
if (texts.some((t) => t.includes(' of b'))) {
controller.abort();
throw new Error('aborted'); // simulates HTTP abort throw
}
return fakeEmbedFn(texts);
},
});
expect(result.aborted).toBe(true);
expect(result.done).toBe(false);
expect(result.embedded).toBe(4); // only 'a' landed
// 'b' and 'c' (8 chunks) remain stale
const stale = await engine.countStaleChunks({ sourceId: 'default' });
expect(stale).toBe(8);
});
test('IRON-RULE: kill + resume — second call picks up via embedding-IS-NULL predicate', async () => {
await seedPageWithStaleChunks('a', 4);
await seedPageWithStaleChunks('b', 4);
// First call aborts when 'b' is reached
const controller = new AbortController();
const first = await embedStaleForSource(engine, 'default', {
batchSize: 4,
concurrency: 1,
signal: controller.signal,
embedFn: async (texts) => {
if (texts.some((t) => t.includes(' of b'))) {
controller.abort();
throw new Error('aborted');
}
return fakeEmbedFn(texts);
},
});
expect(first.aborted).toBe(true);
expect(first.embedded).toBe(4); // 'a' landed
// Second call with NO cursor — predicate excludes already-embedded chunks
const second = await embedStaleForSource(engine, 'default', {
embedFn: fakeEmbedFn,
});
expect(second.done).toBe(true);
expect(first.embedded + second.embedded).toBe(8);
const stale = await engine.countStaleChunks({ sourceId: 'default' });
expect(stale).toBe(0);
});
test('per-page embedFn throw is logged but does NOT propagate', async () => {
await seedPageWithStaleChunks('good', 2);
await seedPageWithStaleChunks('bad', 2);
let badCount = 0;
const result = await embedStaleForSource(engine, 'default', {
embedFn: async (texts) => {
if (texts.some((t) => t.includes('bad'))) {
badCount++;
throw new Error('intentional embed failure');
}
return fakeEmbedFn(texts);
},
});
// The helper itself didn't throw
expect(result.done).toBe(true);
expect(badCount).toBe(1);
// 'good' chunks got embedded; 'bad' chunks stayed NULL
expect(result.embedded).toBe(2);
const stale = await engine.countStaleChunks({ sourceId: 'default' });
expect(stale).toBe(2);
});
test('source-scoped: does not touch other sources', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES ('other', 'other', '{"federated":true}'::jsonb) ON CONFLICT (id) DO NOTHING`,
);
await seedPageWithStaleChunks('a', 3);
await engine.putPage('b', {
type: 'note',
title: 'b',
compiled_truth: '# b\n\nseeded',
}, { sourceId: 'other' });
await engine.upsertChunks(
'b',
Array.from({ length: 3 }, (_, i) => ({
chunk_index: i,
chunk_text: `other ${i}`,
chunk_source: 'compiled_truth',
token_count: 4,
embedding: undefined,
})),
{ sourceId: 'other' },
);
const result = await embedStaleForSource(engine, 'default', {
embedFn: fakeEmbedFn,
});
expect(result.embedded).toBe(3);
// 'other' source still has 3 stale chunks
const otherStale = await engine.countStaleChunks({ sourceId: 'other' });
expect(otherStale).toBe(3);
});
});
+52
View File
@@ -0,0 +1,52 @@
/**
* Tests for src/core/feature-flags.ts (v0.40 D23).
*
* Pin the default-on posture and the explicit-'false'-disables semantics.
*/
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 { isFederatedV2Enabled, FEDERATED_V2_CONFIG_KEY } from '../src/core/feature-flags.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
describe('isFederatedV2Enabled', () => {
test('default (key unset) → enabled', async () => {
expect(await isFederatedV2Enabled(engine)).toBe(true);
});
test('explicit "false" → disabled', async () => {
await engine.setConfig(FEDERATED_V2_CONFIG_KEY, 'false');
expect(await isFederatedV2Enabled(engine)).toBe(false);
});
test('explicit "true" → enabled', async () => {
await engine.setConfig(FEDERATED_V2_CONFIG_KEY, 'true');
expect(await isFederatedV2Enabled(engine)).toBe(true);
});
test('anything-not-literally-false → enabled (defensive default)', async () => {
for (const v of ['False', 'FALSE', '0', 'off', 'no', '']) {
await engine.setConfig(FEDERATED_V2_CONFIG_KEY, v);
expect(await isFederatedV2Enabled(engine)).toBe(true);
}
});
test('config key name is stable', () => {
expect(FEDERATED_V2_CONFIG_KEY).toBe('sync.federated_v2');
});
});
+143
View File
@@ -0,0 +1,143 @@
/**
* Tests for src/core/minions/handlers/embed-backfill.ts (v0.40 D2, D6).
*
* Validates the handler-side contract:
* - Happy path: embeds, returns 'success' with chunk + spend counts
* - D2 lock: second concurrent handler call returns 'already_in_progress'
* - D15.1 finally: lock ALWAYS releases (try/finally even on abort)
*
* Hermetic — uses injected embedFn via the underlying embedStaleForSource
* test seam? No — the handler doesn't expose embedFn passthrough. Instead
* we exercise the handler against a brain with zero stale chunks so no
* actual embed call lands. That gives us a deterministic test of the lock
* + budget + status branches without needing a fake gateway.
*
* The kill-resume contract is covered by test/embed-stale.test.ts at the
* helper layer; the handler just routes through.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { makeEmbedBackfillHandler } from '../src/core/minions/handlers/embed-backfill.ts';
import { tryAcquireDbLock } from '../src/core/db-lock.ts';
import type { MinionJobContext } from '../src/core/minions/types.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
// Clean minion_jobs + lock rows. Preserve config (schema version + flags).
await engine.executeRaw('DELETE FROM minion_jobs');
await engine.executeRaw(`DELETE FROM gbrain_cycle_locks WHERE id LIKE 'gbrain-embed-backfill:%'`);
});
/** Build a minimal MinionJobContext for testing. */
function fakeJob(data: Record<string, unknown>): MinionJobContext {
const controller = new AbortController();
return {
id: 1,
name: 'embed-backfill',
data,
attempts_made: 0,
signal: controller.signal,
shutdownSignal: controller.signal,
updateProgress: async () => {},
updateTokens: async () => {},
log: async () => {},
isActive: async () => true,
readInbox: async () => [],
};
}
describe('embed-backfill handler — happy path', () => {
test('zero stale chunks → success with embedded=0', async () => {
const handler = makeEmbedBackfillHandler(engine);
const result = await handler(fakeJob({ sourceId: 'default' }));
expect(result).toMatchObject({
status: 'success',
sourceId: 'default',
embedded: 0,
chunksProcessed: 0,
pagesProcessed: 0,
});
});
test('throws when sourceId missing', async () => {
const handler = makeEmbedBackfillHandler(engine);
await expect(handler(fakeJob({}))).rejects.toThrow(/sourceId is required/);
});
test('throws when sourceId is empty string', async () => {
const handler = makeEmbedBackfillHandler(engine);
await expect(handler(fakeJob({ sourceId: '' }))).rejects.toThrow(/sourceId is required/);
});
});
describe('embed-backfill handler — D2 lock contract', () => {
test('IRON-RULE: second call returns already_in_progress when lock is held', async () => {
// Hold the per-source lock externally
const lock = await tryAcquireDbLock(engine, 'gbrain-embed-backfill:default', 60);
expect(lock).not.toBeNull();
try {
const handler = makeEmbedBackfillHandler(engine);
const result = await handler(fakeJob({ sourceId: 'default' }));
expect(result).toMatchObject({
status: 'already_in_progress',
sourceId: 'default',
embedded: 0,
spentUsd: 0,
});
} finally {
await lock?.release();
}
});
test('different sources do not contend on each other locks', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES ('other-src', 'other-src', '{"federated":true}') ON CONFLICT (id) DO NOTHING`,
);
const lockA = await tryAcquireDbLock(engine, 'gbrain-embed-backfill:default', 60);
expect(lockA).not.toBeNull();
try {
// 'other-src' should still succeed
const handler = makeEmbedBackfillHandler(engine);
const result = await handler(fakeJob({ sourceId: 'other-src' }));
expect(result.status).toBe('success');
} finally {
await lockA?.release();
}
});
test('IRON-RULE: lock is released after handler completes (try/finally)', async () => {
const handler = makeEmbedBackfillHandler(engine);
await handler(fakeJob({ sourceId: 'default' }));
// After handler returns, the lock row should NOT block a fresh acquire.
const lock = await tryAcquireDbLock(engine, 'gbrain-embed-backfill:default', 60);
expect(lock).not.toBeNull();
await lock?.release();
});
test('IRON-RULE: lock released on throw (sourceId-missing path)', async () => {
const handler = makeEmbedBackfillHandler(engine);
try {
await handler(fakeJob({})); // throws before lock is acquired
} catch {
// expected
}
// Lock was never acquired (throw happened in parseParams pre-lock),
// so the row should be cleanly absent. Verify a fresh acquire works.
const lock = await tryAcquireDbLock(engine, 'gbrain-embed-backfill:default', 60);
expect(lock).not.toBeNull();
await lock?.release();
});
});
+83
View File
@@ -0,0 +1,83 @@
/**
* Tests for src/core/parallel.ts (v0.40 Federated Sync v2).
*
* Pins: result order matches input order, one rejection doesn't kill others,
* concurrency cap is a hard ceiling, empty input returns empty, invalid
* concurrency throws TypeError.
*/
import { describe, test, expect } from 'bun:test';
import { pMapAllSettled } from '../src/core/parallel.ts';
describe('pMapAllSettled', () => {
test('returns results in input order', async () => {
const items = [1, 2, 3, 4, 5];
const results = await pMapAllSettled(items, 2, async (n) => n * 10);
expect(results).toHaveLength(5);
expect(results.map((r) => (r.status === 'fulfilled' ? r.value : null))).toEqual([10, 20, 30, 40, 50]);
});
test('one rejection does not kill other items', async () => {
const items = ['a', 'b', 'c'];
const results = await pMapAllSettled(items, 2, async (s) => {
if (s === 'b') throw new Error('intentional');
return s.toUpperCase();
});
expect(results[0]).toEqual({ status: 'fulfilled', value: 'A' });
expect(results[1].status).toBe('rejected');
expect(results[1].status === 'rejected' && (results[1].reason as Error).message).toBe('intentional');
expect(results[2]).toEqual({ status: 'fulfilled', value: 'C' });
});
test('hard ceiling: never exceeds concurrency in flight', async () => {
let inFlight = 0;
let peak = 0;
const items = Array.from({ length: 20 }, (_, i) => i);
await pMapAllSettled(items, 3, async () => {
inFlight++;
peak = Math.max(peak, inFlight);
await new Promise((r) => setTimeout(r, 5));
inFlight--;
return 'ok';
});
expect(peak).toBeLessThanOrEqual(3);
expect(peak).toBeGreaterThan(0);
});
test('empty input returns empty array', async () => {
const results = await pMapAllSettled([], 5, async () => 'x');
expect(results).toEqual([]);
});
test('concurrency > items.length runs all in parallel (no semaphore stall)', async () => {
const start = Date.now();
const items = [1, 2, 3];
await pMapAllSettled(items, 100, async () => {
await new Promise((r) => setTimeout(r, 50));
return 'ok';
});
const elapsed = Date.now() - start;
// Sequential would be ~150ms; concurrent ~50ms. Allow generous slack.
expect(elapsed).toBeLessThan(140);
});
test('throws TypeError on concurrency < 1', async () => {
await expect(pMapAllSettled([1], 0, async (x) => x)).rejects.toThrow(TypeError);
await expect(pMapAllSettled([1], -3, async (x) => x)).rejects.toThrow(TypeError);
});
test('throws TypeError on non-integer concurrency', async () => {
await expect(pMapAllSettled([1], 1.5, async (x) => x)).rejects.toThrow(TypeError);
await expect(pMapAllSettled([1], NaN, async (x) => x)).rejects.toThrow(TypeError);
});
test('passes index to fn', async () => {
const seen: Array<[string, number]> = [];
await pMapAllSettled(['a', 'b', 'c'], 2, async (item, i) => {
seen.push([item, i]);
return item;
});
// Sort by item since execution order isn't guaranteed.
seen.sort((a, b) => a[0].localeCompare(b[0]));
expect(seen).toEqual([['a', 0], ['b', 1], ['c', 2]]);
});
});
@@ -0,0 +1,42 @@
/**
* v0.40 D16 regression test: phantom-redirect uses per-source lock.
*
* Pre-v0.40 phantom-redirect acquired the bare `gbrain-sync` lock, blocking
* every concurrent sync brain-wide. After D16, phantom acquires
* `gbrain-sync:<sourceId>` — same-source sync still serializes; cross-source
* sync proceeds unblocked.
*
* Pure source-text regression guard. The full behavior is covered by
* `test/e2e/phantom-redirect.test.ts` (DATABASE_URL-gated). This file pins
* the lock-id construction so any future drift back to the bare constant
* fails loudly in the fast unit loop.
*/
import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'node:fs';
import { syncLockId } from '../src/core/db-lock.ts';
const SRC = readFileSync('src/core/cycle/phantom-redirect.ts', 'utf8');
describe('phantom-redirect lock contract', () => {
test('IRON-RULE: import line uses syncLockId, not bare SYNC_LOCK_ID', () => {
// syncLockId must be imported; SYNC_LOCK_ID must NOT be (per-source posture).
const importLine = SRC.split('\n').find((l) =>
l.includes("from '../db-lock.ts'") && l.includes('import'),
);
expect(importLine).toBeDefined();
expect(importLine).toContain('syncLockId');
expect(importLine).not.toMatch(/\bSYNC_LOCK_ID\b/);
});
test('IRON-RULE: acquireLockWithRetry call passes syncLockId(sourceId)', () => {
// Banned: the bare `SYNC_LOCK_ID` constant slipped back in.
// Required: the per-source helper threaded with the active sourceId.
expect(SRC).toMatch(/acquireLockWithRetry\s*\(\s*engine\s*,\s*syncLockId\s*\(\s*sourceId\s*\)\s*\)/);
expect(SRC).not.toMatch(/acquireLockWithRetry\s*\(\s*engine\s*,\s*SYNC_LOCK_ID\s*\)/);
});
test('helper sanity: syncLockId returns gbrain-sync:<source> shape', () => {
expect(syncLockId('default')).toBe('gbrain-sync:default');
expect(syncLockId('zion-brain')).toBe('gbrain-sync:zion-brain');
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* Tests for src/core/source-config-redact.ts (v0.40 D15.4).
*/
import { describe, test, expect } from 'bun:test';
import { redactSourceConfig, hasWebhookSecret } from '../src/core/source-config-redact.ts';
describe('redactSourceConfig', () => {
test('redacts webhook_secret', () => {
const input = { federated: true, webhook_secret: 'super-secret-key', github_repo: 'a/b' };
const out = redactSourceConfig(input);
expect(out.webhook_secret).toBe('<redacted>');
expect(out.federated).toBe(true);
expect(out.github_repo).toBe('a/b');
});
test('does not mutate input', () => {
const input = { webhook_secret: 'secret' };
redactSourceConfig(input);
expect(input.webhook_secret).toBe('secret');
});
test('returns empty object for non-object input', () => {
expect(redactSourceConfig(null)).toEqual({});
expect(redactSourceConfig(undefined)).toEqual({});
expect(redactSourceConfig('a string')).toEqual({});
expect(redactSourceConfig(['arr'])).toEqual({});
});
test('preserves nested objects (not deep-redacted)', () => {
const input = {
webhook_secret: 'x',
nested: { allowed: 'value' },
};
const out = redactSourceConfig(input);
expect(out.webhook_secret).toBe('<redacted>');
expect(out.nested).toEqual({ allowed: 'value' });
});
});
describe('hasWebhookSecret', () => {
test('true when set + non-empty', () => {
expect(hasWebhookSecret({ webhook_secret: 'x' })).toBe(true);
});
test('false when empty', () => {
expect(hasWebhookSecret({ webhook_secret: '' })).toBe(false);
});
test('false when absent', () => {
expect(hasWebhookSecret({})).toBe(false);
});
test('false on non-string values', () => {
expect(hasWebhookSecret({ webhook_secret: 42 })).toBe(false);
expect(hasWebhookSecret(null)).toBe(false);
});
});
+181
View File
@@ -0,0 +1,181 @@
/**
* Tests for src/core/source-health.ts (v0.40 D12 + D9 + D17).
*
* Validates:
* - computeAllSourceMetrics: batched GROUP BY shape, vacuous truth for zero pages
* - resolvePriorityLabel: high/normal/low, unknown → normal + warn-once
* - isSourceStale: never-synced + lag-exceeded + fresh + missing local_path
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import {
computeAllSourceMetrics,
resolvePriorityLabel,
resolvePriority,
isSourceStale,
_resetPriorityWarningsForTest,
} from '../src/core/source-health.ts';
import { loadAllSources } from '../src/core/sources-load.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
// Surgical reset: preserves config table (schema version).
await engine.executeRaw('DELETE FROM minion_jobs');
await engine.executeRaw('DELETE FROM content_chunks');
await engine.executeRaw('DELETE FROM pages');
await engine.executeRaw(`DELETE FROM sources WHERE id != 'default'`);
_resetPriorityWarningsForTest();
});
describe('resolvePriorityLabel', () => {
test('recognized values', () => {
expect(resolvePriorityLabel('s', { priority: 'high' })).toBe('high');
expect(resolvePriorityLabel('s', { priority: 'normal' })).toBe('normal');
expect(resolvePriorityLabel('s', { priority: 'low' })).toBe('low');
});
test('missing → normal silently', () => {
expect(resolvePriorityLabel('s', {})).toBe('normal');
expect(resolvePriorityLabel('s', null)).toBe('normal');
});
test('unknown values → normal with warn', () => {
// Reroute stderr to capture
const orig = process.stderr.write.bind(process.stderr);
let captured = '';
process.stderr.write = ((chunk: string | Uint8Array) => {
captured += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
return true;
}) as never;
try {
expect(resolvePriorityLabel('zion-brain', { priority: 'urgent' })).toBe('normal');
expect(captured).toContain('zion-brain');
expect(captured).toContain('priority');
expect(captured).toContain('normal');
} finally {
process.stderr.write = orig;
}
});
test('warns once per source per process', () => {
const orig = process.stderr.write.bind(process.stderr);
let count = 0;
process.stderr.write = ((chunk: string | Uint8Array) => {
const s = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
if (s.includes('invalid config.priority')) count++;
return true;
}) as never;
try {
resolvePriorityLabel('s1', { priority: 'urgent' });
resolvePriorityLabel('s1', { priority: 'urgent' }); // same source
resolvePriorityLabel('s1', { priority: 42 }); // different bad value, same source
expect(count).toBe(1);
} finally {
process.stderr.write = orig;
}
});
});
describe('resolvePriority (numeric)', () => {
test('maps labels to MinionQueue priority integers', () => {
expect(resolvePriority('s', { priority: 'high' })).toBe(-10);
expect(resolvePriority('s', { priority: 'normal' })).toBe(0);
expect(resolvePriority('s', { priority: 'low' })).toBe(5);
expect(resolvePriority('s', {})).toBe(0);
});
});
describe('isSourceStale', () => {
test('never-synced (last_sync_at null) → true', () => {
const src = { id: 's', name: 's', local_path: '/path', last_commit: null, last_sync_at: null, config: {}, created_at: new Date() };
expect(isSourceStale(src, 60_000)).toBe(true);
});
test('no local_path → false (nothing to sync)', () => {
const src = { id: 's', name: 's', local_path: null, last_commit: null, last_sync_at: null, config: {}, created_at: new Date() };
expect(isSourceStale(src, 60_000)).toBe(false);
});
test('synced within interval → false', () => {
const src = { id: 's', name: 's', local_path: '/path', last_commit: null, last_sync_at: new Date(Date.now() - 1000), config: {}, created_at: new Date() };
expect(isSourceStale(src, 60_000)).toBe(false);
});
test('synced beyond interval → true', () => {
const src = { id: 's', name: 's', local_path: '/path', last_commit: null, last_sync_at: new Date(Date.now() - 120_000), config: {}, created_at: new Date() };
expect(isSourceStale(src, 60_000)).toBe(true);
});
});
describe('computeAllSourceMetrics', () => {
test('empty input returns empty', async () => {
const result = await computeAllSourceMetrics(engine, []);
expect(result).toEqual([]);
});
test('zero-page source → embed_coverage_pct=100 (vacuous truth)', async () => {
const sources = await loadAllSources(engine);
const result = await computeAllSourceMetrics(engine, sources);
const dflt = result.find((m) => m.source_id === 'default')!;
expect(dflt.total_pages).toBe(0);
expect(dflt.total_chunks).toBe(0);
expect(dflt.embed_coverage_pct).toBe(100);
});
test('aggregates pages + chunks + embedding coverage per source', async () => {
// Two pages with chunks, half embedded
await engine.putPage('a', { type: 'note', title: 'a', compiled_truth: 'a' });
await engine.putPage('b', { type: 'note', title: 'b', compiled_truth: 'b' });
await engine.upsertChunks('a', [
{ chunk_index: 0, chunk_text: 'one', chunk_source: 'compiled_truth', token_count: 1, embedding: new Float32Array(1536) },
{ chunk_index: 1, chunk_text: 'two', chunk_source: 'compiled_truth', token_count: 1, embedding: undefined },
]);
await engine.upsertChunks('b', [
{ chunk_index: 0, chunk_text: 'three', chunk_source: 'compiled_truth', token_count: 1, embedding: undefined },
]);
const sources = await loadAllSources(engine);
const result = await computeAllSourceMetrics(engine, sources);
const dflt = result.find((m) => m.source_id === 'default')!;
expect(dflt.total_pages).toBe(2);
expect(dflt.total_chunks).toBe(3);
expect(dflt.embedded_chunks).toBe(1);
// 1/3 = 33.3%
expect(dflt.embed_coverage_pct).toBeCloseTo(33.3, 1);
});
test('lag_seconds is null when last_sync_at is null', async () => {
const sources = await loadAllSources(engine);
const result = await computeAllSourceMetrics(engine, sources);
const dflt = result.find((m) => m.source_id === 'default')!;
expect(dflt.lag_seconds).toBeNull();
});
test('multi-source isolation: each source gets its own counts', async () => {
await engine.executeRaw(`INSERT INTO sources (id, name, config) VALUES ('other', 'other', '{"federated":true}') ON CONFLICT (id) DO NOTHING`);
await engine.putPage('a', { type: 'note', title: 'a', compiled_truth: 'a' });
await engine.putPage('b', { type: 'note', title: 'b', compiled_truth: 'b' }, { sourceId: 'other' });
const sources = await loadAllSources(engine);
const result = await computeAllSourceMetrics(engine, sources);
expect(result.find((m) => m.source_id === 'default')!.total_pages).toBe(1);
expect(result.find((m) => m.source_id === 'other')!.total_pages).toBe(1);
});
test('webhook_configured reflects config.webhook_secret presence', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES ('webhooky', 'webhooky', '{"federated":true,"webhook_secret":"x","github_repo":"a/b"}'::jsonb)`,
);
const sources = await loadAllSources(engine);
const result = await computeAllSourceMetrics(engine, sources);
const w = result.find((m) => m.source_id === 'webhooky')!;
expect(w.webhook_configured).toBe(true);
const d = result.find((m) => m.source_id === 'default')!;
expect(d.webhook_configured).toBe(false);
});
});
+134
View File
@@ -0,0 +1,134 @@
/**
* Tests for src/core/sources-load.ts (v0.40 D7).
*
* Validates the shared loader: ordering, filtering, config parsing, and
* defensive fallback when legacy brains lack the `archived` column.
*/
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 {
loadAllSources,
fetchSource,
parseSourceConfig,
isSourceFederated,
} from '../src/core/sources-load.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
async function insertSource(id: string, opts: { federated?: boolean; archived?: boolean; config?: Record<string, unknown> } = {}): Promise<void> {
const config = { ...opts.config, federated: opts.federated ?? false };
await engine.executeRaw(
`INSERT INTO sources (id, name, config, archived)
VALUES ($1, $1, $2::jsonb, $3)
ON CONFLICT (id) DO UPDATE SET config = EXCLUDED.config, archived = EXCLUDED.archived`,
[id, JSON.stringify(config), opts.archived ?? false],
);
}
describe('loadAllSources', () => {
test('returns default source first, then alphabetical', async () => {
await insertSource('zebra');
await insertSource('alpha');
const rows = await loadAllSources(engine);
expect(rows.map((r) => r.id)).toEqual(['default', 'alpha', 'zebra']);
});
test('excludes archived rows by default', async () => {
await insertSource('keep');
await insertSource('archived-one', { archived: true });
const rows = await loadAllSources(engine);
expect(rows.map((r) => r.id).sort()).toEqual(['default', 'keep']);
});
test('includeArchived: true returns archived rows', async () => {
await insertSource('keep');
await insertSource('archived-one', { archived: true });
const rows = await loadAllSources(engine, { includeArchived: true });
expect(rows.map((r) => r.id).sort()).toEqual(['archived-one', 'default', 'keep']);
});
test('federatedOnly: filters to config.federated=true', async () => {
await insertSource('iso', { federated: false });
await insertSource('fed-a', { federated: true });
await insertSource('fed-b', { federated: true });
const rows = await loadAllSources(engine, { federatedOnly: true });
// default source seeded by resetPgliteState has federated: true
expect(rows.map((r) => r.id).sort()).toEqual(['default', 'fed-a', 'fed-b']);
});
test('rows have the full SourceRow projection (no surprise drift)', async () => {
await insertSource('shapecheck', { federated: true, config: { tracked_branch: 'main' } });
const rows = await loadAllSources(engine);
const target = rows.find((r) => r.id === 'shapecheck');
expect(target).toBeDefined();
expect(target).toMatchObject({
id: 'shapecheck',
name: 'shapecheck',
local_path: null,
last_commit: null,
last_sync_at: null,
});
expect(target!.config).toBeDefined();
expect(target!.created_at).toBeDefined();
});
});
describe('fetchSource', () => {
test('returns row for known id', async () => {
await insertSource('mybrain', { federated: true });
const row = await fetchSource(engine, 'mybrain');
expect(row?.id).toBe('mybrain');
expect(isSourceFederated(row!.config)).toBe(true);
});
test('returns null for unknown id', async () => {
const row = await fetchSource(engine, 'does-not-exist');
expect(row).toBeNull();
});
});
describe('parseSourceConfig', () => {
test('passes through plain object', () => {
expect(parseSourceConfig({ federated: true })).toEqual({ federated: true });
});
test('parses JSON string', () => {
expect(parseSourceConfig('{"federated":true}')).toEqual({ federated: true });
});
test('returns empty object on null / undefined / non-object', () => {
expect(parseSourceConfig(null)).toEqual({});
expect(parseSourceConfig(undefined)).toEqual({});
expect(parseSourceConfig(42)).toEqual({});
});
test('returns empty object on malformed JSON string', () => {
expect(parseSourceConfig('{')).toEqual({});
});
});
describe('isSourceFederated', () => {
test('strict-true requirement', () => {
expect(isSourceFederated({ federated: true })).toBe(true);
expect(isSourceFederated({ federated: false })).toBe(false);
expect(isSourceFederated({ federated: 'true' })).toBe(false); // strict boolean
expect(isSourceFederated({ federated: 1 })).toBe(false);
expect(isSourceFederated({})).toBe(false);
expect(isSourceFederated(null)).toBe(false);
});
});
+125
View File
@@ -0,0 +1,125 @@
/**
* Tests for /webhooks/github HMAC verification semantics (v0.40 T10).
*
* The handler itself is wired inside serve-http.ts's runServeHttp closure
* (hard to invoke without bringing up the full Express app). This file pins
* the load-bearing primitives:
*
* 1. GitHub HMAC sig format matches `sha256=<hex>` via createHmac.
* 2. safeHexEqual is constant-time and rejects mismatches.
* 3. The expected payload schema (repository.full_name + ref) parses.
* 4. Branch ref construction: refs/heads/<tracked_branch>.
*
* The full HTTP path is covered by test/e2e/webhook-github.test.ts
* (DATABASE_URL-gated; not present in this wave — filed as a follow-up
* E2E pinned by the test plan artifact).
*/
import { describe, test, expect } from 'bun:test';
import { createHmac } from 'node:crypto';
import { safeHexEqual } from '../src/core/timing-safe.ts';
const GITHUB_SECRET = 'super-secret-webhook-key';
/** Build a sha256= HMAC sig the way GitHub does. */
function githubSig(secret: string, payload: Buffer): string {
return 'sha256=' + createHmac('sha256', secret).update(payload).digest('hex');
}
/** Strip the GitHub prefix before constant-time hex compare. Matches the
* webhook handler in serve-http.ts. Buffer.from('sha256=...', 'hex') silently
* truncates at non-hex chars; comparing prefixed strings as hex returns
* true-on-anything. The handler strips the prefix; tests must too. */
function verifyGithubSig(secret: string, payload: Buffer, headerSig: string): boolean {
const prefix = 'sha256=';
if (!headerSig.startsWith(prefix)) return false;
const expected = createHmac('sha256', secret).update(payload).digest('hex');
return safeHexEqual(headerSig.slice(prefix.length), expected);
}
/** Synthetic minimal push payload (real GitHub sends ~100KB; we only read 2 fields). */
function pushPayload(repo: string, ref: string): Buffer {
return Buffer.from(JSON.stringify({
ref,
repository: { full_name: repo, name: repo.split('/')[1], owner: { name: repo.split('/')[0] } },
head_commit: { id: 'abc123' },
}), 'utf8');
}
describe('GitHub HMAC verification', () => {
test('valid signature on untampered payload → verify=true', () => {
const payload = pushPayload('Garry-s-List/zion-brain', 'refs/heads/main');
const sig = githubSig(GITHUB_SECRET, payload);
expect(verifyGithubSig(GITHUB_SECRET, payload, sig)).toBe(true);
});
test('IRON-RULE: rejects when signature header lacks sha256= prefix', () => {
// This was the bug: production used safeHexEqual on prefixed strings.
// Buffer.from('sha256=...', 'hex') silently truncates at non-hex chars,
// so both sides decoded to empty buffer and signature_mismatch never fired.
const payload = pushPayload('owner/repo', 'refs/heads/main');
const expected = createHmac('sha256', GITHUB_SECRET).update(payload).digest('hex');
// Without prefix the header is invalid format → reject
expect(verifyGithubSig(GITHUB_SECRET, payload, expected)).toBe(false);
});
test('rejects when secret differs', () => {
const payload = pushPayload('Garry-s-List/zion-brain', 'refs/heads/main');
const goodSig = githubSig(GITHUB_SECRET, payload);
expect(verifyGithubSig('wrong-secret', payload, goodSig)).toBe(false);
});
test('rejects when payload differs (single-byte tamper)', () => {
const payload = pushPayload('Garry-s-List/zion-brain', 'refs/heads/main');
const sig = githubSig(GITHUB_SECRET, payload);
const tampered = Buffer.from(payload);
tampered[10] = tampered[10] ^ 0xff;
expect(verifyGithubSig(GITHUB_SECRET, tampered, sig)).toBe(false);
});
test('GitHub sig format is sha256=<64 hex chars>', () => {
const payload = pushPayload('owner/repo', 'refs/heads/main');
const sig = githubSig(GITHUB_SECRET, payload);
expect(sig).toMatch(/^sha256=[0-9a-f]{64}$/);
});
});
describe('Push payload parsing', () => {
test('extracts repository.full_name + ref', () => {
const payload = pushPayload('Garry-s-List/zion-brain', 'refs/heads/main');
const parsed = JSON.parse(payload.toString('utf8'));
expect(parsed.repository?.full_name).toBe('Garry-s-List/zion-brain');
expect(parsed.ref).toBe('refs/heads/main');
});
test('malformed JSON throws (handler returns 400)', () => {
const bad = Buffer.from('{not json');
expect(() => JSON.parse(bad.toString('utf8'))).toThrow();
});
test('payload missing repository.full_name is detectable', () => {
const partial = Buffer.from(JSON.stringify({ ref: 'refs/heads/main' }));
const parsed = JSON.parse(partial.toString('utf8'));
expect(parsed.repository?.full_name).toBeUndefined();
});
});
describe('Branch ref construction (D5)', () => {
test('default tracked_branch = main produces refs/heads/main', () => {
const trackedBranch = 'main';
expect(`refs/heads/${trackedBranch}`).toBe('refs/heads/main');
});
test('non-main branch is exact match', () => {
const trackedBranch: string = 'master';
expect(`refs/heads/${trackedBranch}`).toBe('refs/heads/master');
// ref="refs/heads/main" against tracked="master" must NOT match
const incoming: string = 'refs/heads/main';
expect(incoming === `refs/heads/${trackedBranch}`).toBe(false);
});
test('feature-branch push to main-tracking source is rejected by exact-match', () => {
const trackedBranch: string = 'main';
const pushedRef: string = 'refs/heads/feature/new-stuff';
expect(pushedRef === `refs/heads/${trackedBranch}`).toBe(false);
});
});
+121
View File
@@ -0,0 +1,121 @@
/**
* Tests for `gbrain sync trigger` CLI (v0.40 D18).
*
* Validates the push-trigger entry point:
* - Help text renders
* - --source <id> required (exits 2)
* - --priority invalid (exits 2)
* - Non-existent source errors before submit (exits 1)
* - Successful submit prints job_id=N on stdout
* - Default priority is high (-10)
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { runSyncTrigger } from '../src/commands/sync.ts';
import { MinionQueue } from '../src/core/minions/queue.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 30000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await engine.executeRaw('DELETE FROM minion_jobs');
});
/** Capture process.exit and stdout/stderr writes for one runSyncTrigger call. */
async function capture(args: string[]): Promise<{
stdout: string;
stderr: string;
exitCode: number | null;
}> {
const origExit = process.exit;
const origLog = console.log;
const origErr = console.error;
let stdout = '';
let stderr = '';
let exitCode: number | null = null;
const exitError = new Error('__exit__');
process.exit = ((code?: number) => {
exitCode = code ?? 0;
throw exitError;
}) as never;
console.log = (...a: unknown[]) => { stdout += a.map(String).join(' ') + '\n'; };
console.error = (...a: unknown[]) => { stderr += a.map(String).join(' ') + '\n'; };
try {
await runSyncTrigger(engine, args);
} catch (e) {
if (e !== exitError) throw e;
} finally {
process.exit = origExit;
console.log = origLog;
console.error = origErr;
}
return { stdout, stderr, exitCode };
}
describe('runSyncTrigger', () => {
test('--help prints usage and returns', async () => {
const { stdout, exitCode } = await capture(['--help']);
expect(exitCode).toBeNull();
expect(stdout).toContain('gbrain sync trigger');
expect(stdout).toContain('--source');
expect(stdout).toContain('--priority');
});
test('missing --source exits 2 with hint', async () => {
const { stderr, exitCode } = await capture([]);
expect(exitCode).toBe(2);
expect(stderr).toContain('--source <id> is required');
});
test('invalid --priority exits 2', async () => {
const { stderr, exitCode } = await capture(['--source', 'default', '--priority', 'urgent']);
expect(exitCode).toBe(2);
expect(stderr).toContain('Invalid --priority value');
});
test('non-existent source exits 1', async () => {
const { stderr, exitCode } = await capture(['--source', 'does-not-exist']);
expect(exitCode).toBe(1);
expect(stderr).toContain('not found');
});
test('valid trigger submits sync job + prints job_id=N to stdout', async () => {
const { stdout, exitCode } = await capture(['--source', 'default']);
expect(exitCode).toBeNull();
expect(stdout).toMatch(/^job_id=\d+$/m);
// Verify a sync job exists with auto_embed_backfill + priority -10
const queue = new MinionQueue(engine);
const jobs = await queue.getJobs({ name: 'sync', limit: 5 });
expect(jobs.length).toBe(1);
const job = jobs[0];
expect(job.priority).toBe(-10);
expect((job.data as { sourceId: string }).sourceId).toBe('default');
expect((job.data as { auto_embed_backfill: boolean }).auto_embed_backfill).toBe(true);
});
test('--priority normal maps to 0', async () => {
const { exitCode } = await capture(['--source', 'default', '--priority', 'normal']);
expect(exitCode).toBeNull();
const queue = new MinionQueue(engine);
const jobs = await queue.getJobs({ name: 'sync', limit: 5 });
expect(jobs[0].priority).toBe(0);
});
test('--priority low maps to 5', async () => {
const { exitCode } = await capture(['--source', 'default', '--priority', 'low']);
expect(exitCode).toBeNull();
const queue = new MinionQueue(engine);
const jobs = await queue.getJobs({ name: 'sync', limit: 5 });
expect(jobs[0].priority).toBe(5);
});
});
+52
View File
@@ -0,0 +1,52 @@
/**
* Tests for src/core/timing-safe.ts (v0.40 D15.5 extraction).
*
* Pure function: behavior tests plus a source-text grep guard that pins the
* extraction so a future drift back to a serve-http-internal closure fails
* loudly. Both the admin-cookie compare AND the webhook HMAC compare MUST
* route through this helper.
*/
import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { safeHexEqual } from '../src/core/timing-safe.ts';
describe('safeHexEqual behavior', () => {
test('returns true for identical hex strings', () => {
const hash = createHash('sha256').update('the-quick-brown-fox').digest('hex');
expect(safeHexEqual(hash, hash)).toBe(true);
});
test('returns false for distinct hex strings of the same length', () => {
const a = createHash('sha256').update('apple').digest('hex');
const b = createHash('sha256').update('banana').digest('hex');
expect(safeHexEqual(a, b)).toBe(false);
});
test('returns false on length mismatch (does NOT throw)', () => {
expect(safeHexEqual('ab', 'abcd')).toBe(false);
expect(safeHexEqual('', 'ab')).toBe(false);
expect(safeHexEqual('abcd', 'ab')).toBe(false);
});
test('empty strings compare equal', () => {
expect(safeHexEqual('', '')).toBe(true);
});
test('Buffer.from(_, "hex") is case-insensitive — same hex bytes match either case', () => {
// Documents Node's hex-decode behavior: 'abcd' and 'ABCD' decode to the
// same bytes, so safeHexEqual returns true. Callers normalize beforehand
// when they need strict case-equality.
expect(safeHexEqual('abcd', 'ABCD')).toBe(true);
});
});
describe('extraction contract', () => {
test('IRON-RULE: serve-http.ts imports safeHexEqual from timing-safe.ts (not redefined)', () => {
const src = readFileSync('src/commands/serve-http.ts', 'utf8');
// Must import from the canonical location
expect(src).toMatch(/import\s*\{[^}]*\bsafeHexEqual\b[^}]*\}\s*from\s*['"]\.\.\/core\/timing-safe\.ts['"]/);
// Must NOT redefine the function inside the file
expect(src).not.toMatch(/function\s+safeHexEqual\s*\(/);
});
});