mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
test(bootstrap): real end-to-end coverage — cross-session recall, per-turn content, Codex door, realistic corpus
Closes the seven e2e gaps a coverage audit surfaced: the plumbing was
well-unit-tested but the product claims ("Codex works, context shows up every
turn with real content, it remembers across restarts, machine two recovers,
Postgres works") were unproven end to end. Test-only wave — zero src changes.
- Hermetic synthetic corpus (test/fixtures/bootstrap-corpus/ + a loader helper):
12 interlinked pages (52 edges, timelines), 12 world/private beliefs, 8 gold
queries — curated from the gbrain-evals synthetic corpora, 100% placeholder
names, so recall is asserted on a real multi-entity brain instead of a
2-node self-planted probe.
- GAP1 magic moment: author a fact via the real write path, disconnect the
engine, reopen against the same DB, recall it — a real session boundary, not
verify.ts's same-connection SQL read-back. Plus a source-isolation assertion.
- GAP2 per-turn content: hook-under-serve Pin 1 now seeds a known fact and
asserts its text lands in the injected block AND private beliefs never do
(was: empty brain, empty_block accepted as a pass).
- GAP3 Codex door: assert the rendered AGENTS.md carries the Gate-3 brain-first
pull protocol; make the fake codex shim implement `mcp get` so the [FIX7]
target-verification can actually fail; the Docker cold-machine harness now
exercises the hooks/MCP registration step instead of skipping it.
- GAP4 corpus recall: turn-context + verify graph-floor/qrels run on the real
multi-entity brain with real edges.
- GAP5 attach: machine-two now re-ingests the cloned brain/ into a fresh DB and
recalls a fact authored only on machine one — the multi-device payoff.
- GAP6 keyed + Postgres (env-gated): real embeddings prove semantic recall a
paraphrase query can reach but keyless BM25 cannot; bootstrap verify drives a
real Postgres engine (skipIf DATABASE_URL/keys absent).
- GAP7 persistence: session-end runs the REAL push (not the mocked seam) to a
local bare remote and the remote receives the content; a planted secret is
blocked at the gate; the 15-min cron installs and fires a scan-gated push.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5183e9f039
commit
6fb6790b2a
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Codex door, end to end (GAP 3a). The Codex harness installs NO per-turn
|
||||
* hooks — the pull protocol in AGENTS.md IS the per-message seam, and the MCP
|
||||
* registration is what wires the tools. Two contracts are load-bearing and
|
||||
* were previously only spot-checked by grepping the "Per-message gates"
|
||||
* heading (a template edit could gut Gate 3's brain-first prose silently):
|
||||
*
|
||||
* 1. The RENDERED AGENTS.md carries Gate 3 ("Entity lookup (brain first)") in
|
||||
* full — search-the-brain-before-answering with the actual brain tools —
|
||||
* so a Codex agent with no hooks still pulls context every turn.
|
||||
* 2. `registerCodexMcp` argv pins `serve --surface full` (bootstrap needs the
|
||||
* whole op surface, not a narrowed `verbs`) and binds GBRAIN_SOURCE so a
|
||||
* GUI-spawned serve (which inherits no shell env) writes to the workspace
|
||||
* source [G1, CX-P1.4].
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { mkdtempSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { renderWorkspace } from '../src/core/bootstrap/render.ts';
|
||||
import { setAnswer } from '../src/core/bootstrap/interview.ts';
|
||||
import { registerCodexMcp } from '../src/core/bootstrap/hooks.ts';
|
||||
|
||||
const REQUIRED_ANSWERS: Record<string, string> = {
|
||||
AGENT_NAME: 'Codexeer',
|
||||
PRINCIPAL_NAME: 'Alice Example',
|
||||
AGENT_PURPOSE: 'Maintain the research corpus and draft the weekly memo.',
|
||||
AGENT_TOP_JOBS: '- corpus upkeep\n- weekly memo\n- meeting prep',
|
||||
PRINCIPAL_CONTEXT: 'Runs a small research lab; ships a memo every Friday.',
|
||||
VOICE_REGISTER: 'Direct. Three options, the second one wins.',
|
||||
};
|
||||
|
||||
function answeredWs(): string {
|
||||
const ws = mkdtempSync(join(tmpdir(), 'gbrain-codex-door-'));
|
||||
for (const [key, value] of Object.entries(REQUIRED_ANSWERS)) {
|
||||
const r = setAnswer(ws, key, value);
|
||||
if (!r.ok) throw new Error(`setup failed for ${key}: ${r.message}`);
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
describe('Codex door — rendered AGENTS.md pull protocol (Gate 3)', () => {
|
||||
test('rendered AGENTS.md carries the full brain-first Entity-lookup gate, not just the heading', () => {
|
||||
const ws = answeredWs();
|
||||
renderWorkspace(ws);
|
||||
const agents = readFileSync(join(ws, 'AGENTS.md'), 'utf8');
|
||||
|
||||
// The heading everyone already greps — necessary but far from sufficient.
|
||||
expect(agents).toContain('Per-message gates');
|
||||
|
||||
// Gate 3 body — the actual pull-protocol prose. If a template edit guts
|
||||
// any of this, a hookless Codex agent stops pulling context and this fails.
|
||||
expect(agents).toContain('Gate 3 — Entity lookup (brain first)');
|
||||
expect(agents).toContain('search the brain before answering');
|
||||
// Names the concrete brain tools, in order (recall → query → get_page).
|
||||
expect(agents).toContain('`recall` for hot');
|
||||
expect(agents).toContain('`query` for synthesis');
|
||||
expect(agents).toContain('`get_page` for the record');
|
||||
// Push-context path: already-injected context is used rather than re-fetched.
|
||||
expect(agents).toContain('If context was already\ninjected this turn, use it.');
|
||||
// The anti-narration + no-generic-grep rule that keeps retrieval silent.
|
||||
expect(agents).toContain('never narrate retrieval');
|
||||
expect(agents).toContain('Never use generic file-grep where a brain tool');
|
||||
|
||||
// The identity token resolved (this is a real render, not the template).
|
||||
expect(agents).toContain('Codexeer');
|
||||
expect(/\{\{[A-Z0-9_]+\}\}/.test(agents)).toBe(false);
|
||||
});
|
||||
|
||||
test('Gate 3 sits inside the Per-message gates block, ahead of the write-back gate', () => {
|
||||
const ws = answeredWs();
|
||||
renderWorkspace(ws);
|
||||
const agents = readFileSync(join(ws, 'AGENTS.md'), 'utf8');
|
||||
const gatesIdx = agents.indexOf('## Per-message gates');
|
||||
const gate3Idx = agents.indexOf('Gate 3 — Entity lookup (brain first)');
|
||||
const gate7Idx = agents.indexOf('Gate 7 — Write-back');
|
||||
expect(gatesIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(gate3Idx).toBeGreaterThan(gatesIdx);
|
||||
expect(gate7Idx).toBeGreaterThan(gate3Idx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Codex door — registerCodexMcp argv (--surface full + GBRAIN_SOURCE)', () => {
|
||||
const BIN = '/opt/gbrain/bin/gbrain';
|
||||
|
||||
test('user-global stdio registration binds the source and pins the full op surface', () => {
|
||||
const argvs = registerCodexMcp({ gbrainBin: BIN, sourceId: 'workspace' });
|
||||
expect(argvs.length).toBe(1);
|
||||
const argv = argvs[0]!;
|
||||
// Exact shape — codex has no scope flag (user-global), env rides the add.
|
||||
expect(argv).toEqual([
|
||||
'codex', 'mcp', 'add', 'gbrain',
|
||||
'--env', 'GBRAIN_SOURCE=workspace',
|
||||
'--', BIN, 'serve', '--surface', 'full',
|
||||
]);
|
||||
// The two contracts, asserted independently so a reshuffle can't hide a drop.
|
||||
const joined = argv.join(' ');
|
||||
expect(joined).toContain('--env GBRAIN_SOURCE=workspace');
|
||||
expect(joined).toContain('serve --surface full');
|
||||
// `serve` argv must NOT narrow to the verbs surface.
|
||||
expect(joined).not.toContain('--surface verbs');
|
||||
});
|
||||
|
||||
test('isolated install also threads GBRAIN_HOME onto the registration [CX2-8]', () => {
|
||||
const argv = registerCodexMcp({ gbrainBin: BIN, sourceId: 'workspace', gbrainHome: '/home/me/agent' })[0]!;
|
||||
expect(argv).toContain('GBRAIN_HOME=/home/me/agent');
|
||||
// GBRAIN_HOME is a second --env, GBRAIN_SOURCE still present, surface intact.
|
||||
expect(argv.filter((a) => a === '--env').length).toBe(2);
|
||||
expect(argv).toContain('GBRAIN_SOURCE=workspace');
|
||||
expect(argv.join(' ')).toContain('serve --surface full');
|
||||
});
|
||||
|
||||
test('a non-absolute gbrain binary is refused (GUI hosts inherit no PATH) [CX-P1.4]', () => {
|
||||
expect(() => registerCodexMcp({ gbrainBin: 'gbrain', sourceId: 'workspace' })).toThrow(/absolute path/);
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
} from '../src/core/bootstrap/verify.ts';
|
||||
import { listVerifyRuns } from '../src/core/bootstrap/status.ts';
|
||||
import type { CapabilityReport } from '../src/core/capability.ts';
|
||||
import { operations, type OperationContext } from '../src/core/operations.ts';
|
||||
import { loadCorpusPages, loadCorpusQueries } from './helpers/bootstrap-corpus.ts';
|
||||
|
||||
const KEYLESS: CapabilityReport = {
|
||||
embeddings: { available: false },
|
||||
@@ -389,3 +391,59 @@ describe('verifyWorkspace — source_id collision resolution', () => {
|
||||
}
|
||||
}, 240_000);
|
||||
});
|
||||
|
||||
describe('verifyWorkspace — real corpus graph floor + qrels recall', () => {
|
||||
test('auto-link builds real multi-entity edges; verify passes on the populated brain; gold queries recall the expected pages', async () => {
|
||||
// Seed the synthetic world into the SAME workspace source verify checks,
|
||||
// through the real put_page handler so auto-link builds REAL edges — a
|
||||
// 12-page graph, not the 2-node self-planted probe pair verify writes.
|
||||
const loaded = await loadCorpusPages(engine, { sourceId: 'workspace' });
|
||||
expect(loaded).toContain('people/alice-example');
|
||||
expect(loaded).toContain('companies/ridge-platform');
|
||||
|
||||
// Real edges exist from the wikilinks in the page bodies (not the probe):
|
||||
// alice-example → ridge-platform, with multiple outbound edges on a real
|
||||
// entity, and the reverse edge answers through the backlink table.
|
||||
const aliceLinks = await engine.getLinks('people/alice-example', { sourceId: 'workspace' });
|
||||
const aliceTargets = aliceLinks.map((l) => l.to_slug);
|
||||
expect(aliceTargets).toContain('companies/ridge-platform');
|
||||
expect(aliceTargets.length).toBeGreaterThanOrEqual(2);
|
||||
const ridgeBacklinks = await engine.getBacklinks('companies/ridge-platform', { sourceId: 'workspace' });
|
||||
expect(ridgeBacklinks.map((l) => l.from_slug)).toContain('people/alice-example');
|
||||
|
||||
// verify still runs green end-to-end on the now-populated brain, and its
|
||||
// graph_floor / roundtrip checks pass alongside the real corpus graph.
|
||||
const res = await verifyWorkspace(engine, ws, {
|
||||
sourceId: 'workspace',
|
||||
gbrainHomeDir: home,
|
||||
capabilities: KEYLESS,
|
||||
skipHooksSmoke: true,
|
||||
});
|
||||
if (!res.ok) console.error(res.report);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(check(res.checks, 'graph_floor')[0].ok).toBe(true);
|
||||
for (const c of check(res.checks, 'roundtrip')) expect(c.ok).toBe(true);
|
||||
|
||||
// qrels: each gold page-recall case returns its expected slug through the
|
||||
// REAL keyword-search query op (keyless), over the multi-page corpus.
|
||||
const queryOp = operations.find((o) => o.name === 'query')!;
|
||||
const ctx: OperationContext = {
|
||||
engine,
|
||||
config: { engine: 'pglite' } as never,
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId: 'workspace',
|
||||
};
|
||||
const goldPageCases = loadCorpusQueries().filter((q) => q.kind === 'page');
|
||||
expect(goldPageCases.length).toBeGreaterThan(0);
|
||||
const misses: string[] = [];
|
||||
for (const q of goldPageCases) {
|
||||
const result = await queryOp.handler(ctx, { query: q.query, limit: 10, expand: false });
|
||||
if (!JSON.stringify(result).includes(q.expect_slug!)) {
|
||||
misses.push(`${q.id}: "${q.query}" did not recall ${q.expect_slug}`);
|
||||
}
|
||||
}
|
||||
expect(misses).toEqual([]);
|
||||
}, 240_000);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { hardenBrainRepo } from '../src/core/brain-repo-durability.ts';
|
||||
import { hardenBrainRepo, unhardenBrainRepo } from '../src/core/brain-repo-durability.ts';
|
||||
import { runPull } from '../src/commands/sources-harden.ts';
|
||||
|
||||
// #2943 root cause: `env: process.env` is REQUIRED here. Bun snapshots
|
||||
// process.env at startup, so without it the spawned git — and any post-commit
|
||||
@@ -192,3 +193,65 @@ describe('post-commit hook (D9 local, D7 self-contained)', () => {
|
||||
expect(found).toBe(true);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
// The persistence schedule is the DB-free PULL cron (D2/D12) — the push side
|
||||
// is the post-commit hook proved above. beforeEach hardens with
|
||||
// installCron:false; this block re-hardens the SAME repo with installCron:true
|
||||
// and proves the scheduled job is registered with the right command + interval,
|
||||
// then invokes that exact command to prove it performs a real pull. It always
|
||||
// unregisters the launchd/cron job afterward so no scheduled job survives.
|
||||
describe('durability schedule (installCron:true) [D2/D12]', () => {
|
||||
test('registers the DB-free pull job with the right command + interval, and the job performs a real pull', async () => {
|
||||
const sourceId = 'wiki';
|
||||
const report = await hardenBrainRepo({
|
||||
repoPath: work, sourceId, pat: 'ghp_x', installCron: true, intervalSec: 900, verify: false,
|
||||
});
|
||||
const toplevel = git(work, 'rev-parse', '--show-toplevel');
|
||||
try {
|
||||
const cronStep = report.steps.find((s) => s.step === 'cron')!;
|
||||
expect(cronStep).toBeDefined();
|
||||
expect(cronStep.status).not.toBe('skipped'); // installCron:true → it ran
|
||||
|
||||
// The scheduled COMMAND: a DB-free `sources pull` wrapper for THIS repo,
|
||||
// written to <home>/brain-pull-<sourceId>.sh regardless of platform.
|
||||
const wrapper = join(process.env.HOME!, '.gbrain', `brain-pull-${sourceId}.sh`);
|
||||
expect(existsSync(wrapper)).toBe(true);
|
||||
const body = readFileSync(wrapper, 'utf-8');
|
||||
expect(body).toContain(`sources pull --path '${toplevel}' --branch 'main'`);
|
||||
|
||||
// The REGISTERED job + its 15-minute INTERVAL. launchd (darwin) is
|
||||
// deterministic — assert the plist directly; the step detail names the
|
||||
// interval on the darwin path.
|
||||
if (process.platform === 'darwin') {
|
||||
const plist = join(process.env.HOME!, 'Library', 'LaunchAgents', `com.gbrain.brain-pull.${sourceId}.plist`);
|
||||
expect(existsSync(plist)).toBe(true);
|
||||
const xml = readFileSync(plist, 'utf-8');
|
||||
expect(xml).toContain('<key>StartInterval</key><integer>900</integer>'); // 900s = 15m
|
||||
expect(xml).toContain(wrapper); // ProgramArguments points at our wrapper
|
||||
expect(cronStep.detail).toContain('900s');
|
||||
}
|
||||
|
||||
// Invoke the scheduled command directly to PROVE it performs the pull.
|
||||
// Advance origin from a second clone, then run the exact DB-free pull the
|
||||
// wrapper execs (`gbrain sources pull --path <repo> --branch main`).
|
||||
const other = mkdtempSync(join(root, 'other-'));
|
||||
execFileSync('git', ['-c', 'protocol.file.allow=always', 'clone', '-q', bare, other], { stdio: 'ignore', env: process.env });
|
||||
git(other, 'config', 'user.email', 'o@o.o'); git(other, 'config', 'user.name', 'other');
|
||||
writeFileSync(join(other, 'from-remote.md'), 'landed via a second device\n');
|
||||
git(other, 'add', 'from-remote.md'); git(other, 'commit', '-qm', 'remote advance'); git(other, 'push', '-q', 'origin', 'main');
|
||||
|
||||
const remoteHead = originHead(bare);
|
||||
expect(git(work, 'rev-parse', 'HEAD')).not.toBe(remoteHead); // local is behind
|
||||
|
||||
await runPull(null, ['--path', work, '--branch', 'main']);
|
||||
|
||||
// The scheduled pull fast-forwarded the local checkout to the remote and
|
||||
// the remote-authored file is now present locally.
|
||||
expect(git(work, 'rev-parse', 'HEAD')).toBe(remoteHead);
|
||||
expect(existsSync(join(work, 'from-remote.md'))).toBe(true);
|
||||
} finally {
|
||||
// Unregister while HOME is still the temp home (afterEach restores it).
|
||||
await unhardenBrainRepo({ repoPath: work, sourceId });
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
@@ -35,6 +35,26 @@ import { attachWorkspace } from '../../src/core/bootstrap/attach.ts';
|
||||
import { readManifest, readReceipt } from '../../src/core/bootstrap/format.ts';
|
||||
import { initState, setAnswer, confirm, readBackHash } from '../../src/core/bootstrap/interview.ts';
|
||||
import { realpathOrResolve } from '../../src/core/path-confine.ts';
|
||||
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
|
||||
import { addSource } from '../../src/core/sources-ops.ts';
|
||||
import { importFromFile } from '../../src/core/import-file.ts';
|
||||
import { verifyWorkspace } from '../../src/core/bootstrap/verify.ts';
|
||||
import { operations, type OperationContext } from '../../src/core/operations.ts';
|
||||
import type { CapabilityReport } from '../../src/core/capability.ts';
|
||||
import { CORPUS_DIR } from '../helpers/bootstrap-corpus.ts';
|
||||
|
||||
/** Keyless capability report — machine 2 re-ingests + verifies with ZERO keys. */
|
||||
const KEYLESS: CapabilityReport = {
|
||||
embeddings: { available: false },
|
||||
extraction: { available: false },
|
||||
search: 'keyword-only',
|
||||
mode: 'keyless',
|
||||
};
|
||||
|
||||
/** A page authored ONLY on machine 1, committed to the repo, cloned to machine 2. */
|
||||
const MACHINE1_PAGE_SLUG = 'companies/summit-robotics';
|
||||
/** A distinctive fact that exists nowhere but machine 1's authored repo page. */
|
||||
const MACHINE1_MARKER = 'The flagship warehouse pilot runs at the Rivermouth fulfillment center.';
|
||||
|
||||
const SAVED_ENV: Record<string, string | undefined> = {};
|
||||
const ENV_KEYS = [
|
||||
@@ -129,6 +149,17 @@ beforeAll(async () => {
|
||||
expect(renderCode).toBe(0);
|
||||
expect(readManifest(ws1).state).toBe('initialized');
|
||||
|
||||
// Author a corpus page into machine 1's brain/ BEFORE the commit — a real
|
||||
// synthetic page carrying a machine-1-only fact (the Rivermouth marker). It
|
||||
// lives ONLY in the repo, so machine 2 can only learn it by re-ingesting the
|
||||
// clone [multi-device promise: hot facts arrive via the repo].
|
||||
const corpusPage = readFileSync(join(CORPUS_DIR, 'pages', 'companies__summit-robotics.md'), 'utf8');
|
||||
mkdirSync(join(ws1, 'brain', 'companies'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(ws1, 'brain', 'companies', 'summit-robotics.md'),
|
||||
`${corpusPage.trimEnd()}\n\n## Operations\n\n${MACHINE1_MARKER}\n`,
|
||||
);
|
||||
|
||||
// git init + commit + local bare 'origin' + clone → machine 2.
|
||||
git(ws1, ['init', '-q', '-b', 'main']);
|
||||
git(ws1, ['config', 'user.email', 'test@example.com']);
|
||||
@@ -239,6 +270,81 @@ describe('bootstrap attach (machine-2 adoption, serial e2e)', () => {
|
||||
]);
|
||||
}, 60_000);
|
||||
|
||||
test('machine-2 full round-trip: repo page re-ingests into a FRESH brain, verify passes, and a machine-1-only fact is recalled', async () => {
|
||||
// A fresh machine-2 PGLite brain — nothing pre-seeded. The only path a fact
|
||||
// can reach it is the cloned repo tree. Hermetic in-memory engine (the
|
||||
// proven in-process verify pattern) so teardown is clean.
|
||||
const engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
try {
|
||||
// Register the cloned brain/ as the workspace source (what `bootstrap
|
||||
// attach` → `gbrain sources add` does on machine 2).
|
||||
await addSource(engine, { id: 'workspace', localPath: join(ws2, 'brain'), force: true });
|
||||
|
||||
// Precondition: the machine-1-authored page is NOT in the fresh DB — it
|
||||
// exists ONLY as a committed repo file cloned from machine 1.
|
||||
expect(await engine.getPage(MACHINE1_PAGE_SLUG, { sourceId: 'workspace' })).toBeNull();
|
||||
const clonedFile = join(ws2, 'brain', 'companies', 'summit-robotics.md');
|
||||
expect(existsSync(clonedFile)).toBe(true);
|
||||
expect(readFileSync(clonedFile, 'utf8')).toContain(MACHINE1_MARKER);
|
||||
|
||||
// Re-ingest through the REAL per-file import path (the primitive `gbrain
|
||||
// sync` runs on each changed file), scoped to the workspace source.
|
||||
const rel = join('companies', 'summit-robotics.md');
|
||||
const imp = await importFromFile(engine, clonedFile, rel, { sourceId: 'workspace', noEmbed: true });
|
||||
expect(imp.status).toBe('imported');
|
||||
expect(imp.slug).toBe(MACHINE1_PAGE_SLUG);
|
||||
|
||||
// The page + its machine-1-authored content now live in machine 2's DB.
|
||||
const page = await engine.getPage(MACHINE1_PAGE_SLUG, { sourceId: 'workspace' });
|
||||
expect(page).not.toBeNull();
|
||||
expect(page!.title).toBe('Summit Robotics');
|
||||
expect(page!.compiled_truth ?? '').toContain(MACHINE1_MARKER);
|
||||
|
||||
// The clone's origin is a local bare path gh cannot prove private; drop it
|
||||
// so repo_privacy resolves to the honest local-only pass. The multi-device
|
||||
// fact transport under test is orthogonal to remote-privacy verification.
|
||||
execFileSync('git', ['-C', ws2, 'remote', 'remove', 'origin']);
|
||||
|
||||
// `bootstrap verify` core runs GREEN on machine 2, keyless.
|
||||
const res = await verifyWorkspace(engine, ws2, {
|
||||
sourceId: 'workspace',
|
||||
gbrainHomeDir: join(machine2Home, '.gbrain'),
|
||||
capabilities: KEYLESS,
|
||||
skipHooksSmoke: true,
|
||||
});
|
||||
if (!res.ok) console.error(res.report);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.checks.find((c) => c.id === 'roundtrip')!.ok).toBe(true);
|
||||
|
||||
// Recall the machine-1-only fact through the REAL keyword-search query op.
|
||||
// The Rivermouth marker was authored on machine 1 and reached machine 2
|
||||
// ONLY via the repo, so a hit proves the multi-device promise end to end.
|
||||
const queryOp = operations.find((o) => o.name === 'query')!;
|
||||
const ctx: OperationContext = {
|
||||
engine,
|
||||
config: { engine: 'pglite' } as never,
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId: 'workspace',
|
||||
};
|
||||
const markerHit = await queryOp.handler(ctx, { query: 'Rivermouth fulfillment center warehouse', limit: 10, expand: false });
|
||||
expect(JSON.stringify(markerHit)).toContain(MACHINE1_PAGE_SLUG);
|
||||
const goldHit = await queryOp.handler(ctx, { query: 'warehouse navigation robots startup', limit: 10, expand: false });
|
||||
expect(JSON.stringify(goldHit)).toContain(MACHINE1_PAGE_SLUG);
|
||||
} finally {
|
||||
// The query op fires retrieval telemetry (last_retrieved_at / search
|
||||
// stats) as a background write. PGLite serializes one connection, so a
|
||||
// yield-then-flush drains that in-flight write BEFORE close() — issuing
|
||||
// db.close() with a query still queued otherwise deadlocks disconnect.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
await engine.executeRaw('SELECT 1').catch(() => {});
|
||||
await engine.disconnect();
|
||||
}
|
||||
}, 240_000);
|
||||
|
||||
test('template clone (initialized: false) is REFUSED with the agent-readable render pointer [CX2-1]', async () => {
|
||||
const ws3 = mkdtempSync(join(tmpdir(), 'gb-att-ws3-'));
|
||||
try {
|
||||
|
||||
@@ -39,6 +39,12 @@ import { resolveSocketPath, ipcSecretPath } from '../../src/core/context/resolve
|
||||
import { LiveServeLockError } from '../../src/core/pglite-lock.ts';
|
||||
import { createEngine } from '../../src/core/engine-factory.ts';
|
||||
import { addSource } from '../../src/core/sources-ops.ts';
|
||||
import {
|
||||
loadCorpusPages,
|
||||
loadCorpusBeliefs,
|
||||
loadCorpusBeliefData,
|
||||
loadCorpusQueries,
|
||||
} from '../helpers/bootstrap-corpus.ts';
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dir, '..', '..');
|
||||
const TRANSCRIPT_FIXTURE = join(REPO_ROOT, 'test', 'fixtures', 'conversation-formats', 'claude-code.jsonl');
|
||||
@@ -120,12 +126,20 @@ beforeAll(async () => {
|
||||
mkdirSync(join(ws, 'brain'), { recursive: true });
|
||||
|
||||
// Pre-init the brain in-process (schema + source) so the serve subprocess
|
||||
// boots fast and doesn't spend its boot budget on migrations.
|
||||
// boots fast and doesn't spend its boot budget on migrations. Also SEED the
|
||||
// synthetic corpus (pages + world/private beliefs) into the serve's source
|
||||
// BEFORE the serve takes the single-writer lock — the serve then reads this
|
||||
// committed data when it assembles turn context, so Pin 1 can assert on real
|
||||
// recalled content (world belief present, private belief fenced out) instead
|
||||
// of accepting an empty block [GAP 2].
|
||||
const engineConfig = { engine: 'pglite' as const, database_path: dbDir };
|
||||
const engine = await createEngine(engineConfig);
|
||||
await engine.connect(engineConfig);
|
||||
await engine.initSchema();
|
||||
await addSource(engine, { id: 'workspace', localPath: join(ws, 'brain'), force: true });
|
||||
await loadCorpusPages(engine, { sourceId: 'workspace' });
|
||||
const seededBeliefs = await loadCorpusBeliefs(engine, { sourceId: 'workspace' });
|
||||
if (seededBeliefs < 1) throw new Error('corpus beliefs failed to seed — GAP 2 has nothing to recall');
|
||||
await engine.disconnect();
|
||||
|
||||
// REAL serve subprocess. stdin stays open (a stdin EOF is serve's shutdown
|
||||
@@ -198,17 +212,25 @@ describe('bootstrap hook under a live serve (serial e2e) [A7]', () => {
|
||||
expect(existsSync(ipcSecretPath(dbDir))).toBe(true);
|
||||
}, 30_000);
|
||||
|
||||
test('Pin 1: user-prompt hook with a LIVE serve → exit 0 + context JSON or a documented degradation heartbeat', async () => {
|
||||
test('Pin 1: user-prompt hook with a LIVE serve → exit 0 + additionalContext carrying the recalled WORLD belief, never the PRIVATE one', async () => {
|
||||
// Transcript fixture under the confinement seam root.
|
||||
const projRoot = join(tmpParent, 'projects');
|
||||
mkdirSync(join(projRoot, 'p1'), { recursive: true });
|
||||
const transcript = join(projRoot, 'p1', 'session.jsonl');
|
||||
copyFileSync(TRANSCRIPT_FIXTURE, transcript);
|
||||
|
||||
// Drive the turn off a real gold recall case: its query is the prompt, its
|
||||
// expected substring is a WORLD belief we seeded, and its must_not is the
|
||||
// PRIVATE sibling belief the visibility fence must keep out of the block.
|
||||
const beliefCase = loadCorpusQueries().find((q) => q.id === 'belief-recall-alice-standups');
|
||||
expect(beliefCase).toBeDefined();
|
||||
expect(beliefCase!.expect_substring).toBeDefined();
|
||||
expect(beliefCase!.must_not_substring).toBeDefined();
|
||||
|
||||
const out = collectStdout();
|
||||
const code = await runHook(['user-prompt'], {
|
||||
stdin: JSON.stringify({
|
||||
prompt: 'what do we know about widget-co?',
|
||||
prompt: beliefCase!.query,
|
||||
session_id: 'hook-under-serve-1',
|
||||
transcript_path: transcript,
|
||||
}),
|
||||
@@ -224,16 +246,28 @@ describe('bootstrap hook under a live serve (serial e2e) [A7]', () => {
|
||||
expect(hb.event).toBe('user-prompt');
|
||||
expect(hb.outcome).not.toBe('error');
|
||||
|
||||
// Every distinctive fragment of a PRIVATE belief that must NEVER cross the
|
||||
// IPC boundary (the meta-hook pins visibility=['world'] for the hook path).
|
||||
const privateFragments = loadCorpusBeliefData()
|
||||
.filter((b) => b.visibility === 'private')
|
||||
.map((b) => b.text);
|
||||
|
||||
if (payload.length > 0) {
|
||||
// Non-empty additionalContext JSON — the full happy-path contract.
|
||||
// Happy path: assert the REAL recalled content, not length>0. The
|
||||
// assembled block must carry the seeded WORLD belief the gold case
|
||||
// expects, and NONE of the private beliefs.
|
||||
const parsed = JSON.parse(payload) as {
|
||||
hookSpecificOutput: { hookEventName: string; additionalContext: string };
|
||||
};
|
||||
expect(parsed.hookSpecificOutput.hookEventName).toBe('UserPromptSubmit');
|
||||
expect(parsed.hookSpecificOutput.additionalContext.length).toBeGreaterThan(0);
|
||||
const ctx = parsed.hookSpecificOutput.additionalContext;
|
||||
expect(ctx).toContain(beliefCase!.expect_substring!);
|
||||
expect(ctx).not.toContain(beliefCase!.must_not_substring!);
|
||||
for (const frag of privateFragments) expect(ctx).not.toContain(frag);
|
||||
} else {
|
||||
// Empty stdout MUST be a documented degradation recorded in the
|
||||
// heartbeat — never a silent nothing, never a wiring-broken reason.
|
||||
// heartbeat — never a silent nothing, never a wiring-broken reason. A
|
||||
// degraded turn also, trivially, leaked no private content.
|
||||
expect(hb.reason).toBeDefined();
|
||||
expect(DOCUMENTED_LIVE_SERVE_REASONS.has(hb.reason!)).toBe(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* GAP 6 — keyed mode + Postgres bootstrap coverage.
|
||||
*
|
||||
* Every other bootstrap test pins the KEYLESS posture (embeddings.available:
|
||||
* false) against embedded PGLite with DATABASE_URL stripped. Nothing proved
|
||||
* that (a) a keyed brain actually does SEMANTIC recall that BM25 can't, that
|
||||
* (b) a real chat key drives auto fact-extraction the keyless path can't, or
|
||||
* that (c) the bootstrap verify probes run on a real Postgres/Supabase engine
|
||||
* instead of only PGLite. This file closes all three, each env-gated with
|
||||
* `skipIf` so a CI run without secrets stays green while the path runs for
|
||||
* real when the key / DATABASE_URL is present.
|
||||
*
|
||||
* Serial: mutates process.env.GBRAIN_HOME and the process-global AI gateway.
|
||||
* Deliberately does NOT strip DATABASE_URL (the Postgres describe needs it).
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
|
||||
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
|
||||
import { addSource } from '../../src/core/sources-ops.ts';
|
||||
import { loadCorpusPages } from '../helpers/bootstrap-corpus.ts';
|
||||
import { runEmbedCore } from '../../src/commands/embed.ts';
|
||||
import { hybridSearch } from '../../src/core/search/hybrid.ts';
|
||||
import { runSchemaTransition } from '../../src/core/retrieval-upgrade-planner.ts';
|
||||
import { extractTakesFromPages } from '../../src/core/extract-takes-from-pages.ts';
|
||||
import { configureGateway, resetGateway } from '../../src/core/ai/gateway.ts';
|
||||
import {
|
||||
verifyWorkspace,
|
||||
VERIFY_PROBE_SLUG,
|
||||
VERIFY_PROBE_ENTITY_SLUG,
|
||||
VERIFY_MAGIC_TOKEN,
|
||||
} from '../../src/core/bootstrap/verify.ts';
|
||||
import type { CapabilityReport } from '../../src/core/capability.ts';
|
||||
|
||||
const OPENAI = process.env.OPENAI_API_KEY;
|
||||
const VOYAGE = process.env.VOYAGE_API_KEY;
|
||||
const ANTHROPIC = process.env.ANTHROPIC_API_KEY;
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
const KEYLESS: CapabilityReport = {
|
||||
embeddings: { available: false },
|
||||
extraction: { available: false },
|
||||
search: 'keyword-only',
|
||||
mode: 'keyless',
|
||||
};
|
||||
|
||||
/** Resolve the embedding provider the same way `gbrain init` would from env.
|
||||
* OpenAI (1536d) matches the committed `embedding` column width, so no schema
|
||||
* transition is needed; Voyage (1024d) requires resizing the column exactly
|
||||
* the way the provider-agnostic migration does. */
|
||||
function resolveEmbedProvider():
|
||||
| { model: string; dims: number; env: Record<string, string>; cfg: Record<string, string> }
|
||||
| null {
|
||||
if (OPENAI) {
|
||||
return {
|
||||
model: 'openai:text-embedding-3-large',
|
||||
dims: 1536,
|
||||
env: { OPENAI_API_KEY: OPENAI },
|
||||
cfg: { openai_api_key: OPENAI },
|
||||
};
|
||||
}
|
||||
if (VOYAGE) {
|
||||
return {
|
||||
model: 'voyage:voyage-3-large',
|
||||
dims: 1024,
|
||||
env: { VOYAGE_API_KEY: VOYAGE },
|
||||
cfg: { voyage_api_key: VOYAGE },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Chat provider for the LLM fact-extraction path (embeddings not required). */
|
||||
function resolveChatProvider():
|
||||
| { model: string; env: Record<string, string> }
|
||||
| null {
|
||||
if (ANTHROPIC) return { model: 'anthropic:claude-haiku-4-5', env: { ANTHROPIC_API_KEY: ANTHROPIC } };
|
||||
if (OPENAI) return { model: 'openai:gpt-4o-mini', env: { OPENAI_API_KEY: OPENAI } };
|
||||
return null;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// (a) Keyed semantic recall: a PARAPHRASE query (no shared keyword with the
|
||||
// target page) recalls the right page via real embeddings — something the
|
||||
// keyless BM25 path structurally cannot do (disjoint tokens ⇒ zero lexical
|
||||
// hits). The contrast is asserted directly: the keyword arm returns the
|
||||
// page NOT AT ALL, the vector arm surfaces it.
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
describe.skipIf(!OPENAI && !VOYAGE)('keyed semantic recall (real embeddings)', () => {
|
||||
let engine: PGLiteEngine;
|
||||
let home: string;
|
||||
let root: string;
|
||||
let prevHome: string | undefined;
|
||||
let embedded = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
const prov = resolveEmbedProvider()!;
|
||||
root = mkdtempSync(join(tmpdir(), 'gb-keyed-'));
|
||||
home = join(root, '.gbrain');
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(
|
||||
join(home, 'config.json'),
|
||||
JSON.stringify({
|
||||
engine: 'pglite',
|
||||
embedding_model: prov.model,
|
||||
embedding_dimensions: prov.dims,
|
||||
...prov.cfg,
|
||||
}),
|
||||
);
|
||||
prevHome = process.env.GBRAIN_HOME;
|
||||
process.env.GBRAIN_HOME = root;
|
||||
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
// Size the primary embedding column to the provider's width (OpenAI is
|
||||
// already 1536; Voyage needs the resize the real migration performs).
|
||||
if (prov.dims !== 1536) await runSchemaTransition(engine, prov.dims);
|
||||
await addSource(engine, { id: 'workspace', localPath: join(root, 'brain'), force: true });
|
||||
|
||||
// Load the corpus KEYLESS (no gateway yet, so put_page never auto-embeds),
|
||||
// then configure the real gateway and run the embed sweep for real.
|
||||
mkdirSync(join(root, 'brain'), { recursive: true });
|
||||
await loadCorpusPages(engine, { sourceId: 'workspace' });
|
||||
configureGateway({ embedding_model: prov.model, embedding_dimensions: prov.dims, env: prov.env });
|
||||
const res = await runEmbedCore(engine, { stale: true, sourceId: 'workspace', quiet: true });
|
||||
embedded = res.embedded;
|
||||
}, 300_000);
|
||||
|
||||
afterAll(async () => {
|
||||
try { await engine.disconnect(); } catch { /* noop */ }
|
||||
resetGateway();
|
||||
if (prevHome === undefined) delete process.env.GBRAIN_HOME;
|
||||
else process.env.GBRAIN_HOME = prevHome;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('the embed sweep actually produced vectors', () => {
|
||||
// Guards against a false-green semantic assertion below: if nothing
|
||||
// embedded, a "recall" could only be lexical.
|
||||
expect(embedded).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Each case: a query whose tokens are DISJOINT from the target page so BM25
|
||||
// cannot reach it; only a real embedding can. Asserted both ways.
|
||||
const cases = [
|
||||
{
|
||||
name: 'summit-robotics',
|
||||
slug: 'companies/summit-robotics',
|
||||
paraphrase: 'company making self-guided fulfillment-center machinery',
|
||||
},
|
||||
{
|
||||
name: 'hybrid-retrieval',
|
||||
slug: 'concepts/hybrid-retrieval',
|
||||
paraphrase: 'combining vector similarity with lexical token matching for lookup',
|
||||
},
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
test(`paraphrase "${c.paraphrase.slice(0, 32)}…" recalls ${c.name} via vectors, not keywords`, async () => {
|
||||
// The keyless arm: pure BM25. Disjoint tokens ⇒ the target is unreachable.
|
||||
const kw = await engine.searchKeyword(c.paraphrase, { limit: 10, sourceId: 'workspace' });
|
||||
expect(kw.map((r) => r.slug)).not.toContain(c.slug);
|
||||
|
||||
// The keyed arm: real query embedding + vector search surfaces it.
|
||||
const results = await hybridSearch(engine, c.paraphrase, {
|
||||
limit: 10,
|
||||
sourceId: 'workspace',
|
||||
mode: 'balanced',
|
||||
});
|
||||
const slugs = results.map((r) => r.slug);
|
||||
expect(slugs).toContain(c.slug);
|
||||
// And it ranks as a confident hit, not a tail match.
|
||||
expect(slugs.slice(0, 5)).toContain(c.slug);
|
||||
}, 120_000);
|
||||
}
|
||||
});
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// (b) Keyed auto fact-extraction: a real chat key drives the takes classifier
|
||||
// over concept prose and lands a gradeable claim. The keyless path returns
|
||||
// `llm_unavailable` with zero claims — the exact capability a key unlocks.
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
describe.skipIf(!OPENAI && !ANTHROPIC)('keyed auto fact-extraction (LLM takes)', () => {
|
||||
let engine: PGLiteEngine;
|
||||
let home: string;
|
||||
let root: string;
|
||||
let prevHome: string | undefined;
|
||||
const SLUG = 'concepts/keyed-extraction-probe';
|
||||
// >200 chars of concept prose carrying clear gradeable claims (facts/takes),
|
||||
// so the classifier has something real to extract.
|
||||
const PROSE = [
|
||||
'Hybrid retrieval systems consistently outperform pure keyword search on',
|
||||
'recall-sensitive corpora because dense embeddings capture paraphrase',
|
||||
'relationships that sparse lexical matching misses. The reranker stage is',
|
||||
'the single highest-leverage component: teams that skip it leave roughly a',
|
||||
'third of achievable precision on the table. In 2026 most production memory',
|
||||
'stacks will standardize on a reranked hybrid pipeline as the default, and',
|
||||
'brains that stay keyword-only will feel noticeably worse to their users.',
|
||||
].join(' ');
|
||||
|
||||
beforeAll(async () => {
|
||||
root = mkdtempSync(join(tmpdir(), 'gb-extract-'));
|
||||
home = join(root, '.gbrain');
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(join(home, 'config.json'), JSON.stringify({ engine: 'pglite' }));
|
||||
prevHome = process.env.GBRAIN_HOME;
|
||||
process.env.GBRAIN_HOME = root;
|
||||
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
await engine.putPage(SLUG, {
|
||||
type: 'concept',
|
||||
title: 'Keyed Extraction Probe',
|
||||
compiled_truth: PROSE,
|
||||
});
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
try { await engine.disconnect(); } catch { /* noop */ }
|
||||
resetGateway();
|
||||
if (prevHome === undefined) delete process.env.GBRAIN_HOME;
|
||||
else process.env.GBRAIN_HOME = prevHome;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('keyless → llm_unavailable, zero claims; keyed → a real fact/take lands', async () => {
|
||||
// Keyless contrast: no chat gateway ⇒ the sweep cannot extract anything.
|
||||
resetGateway();
|
||||
const keyless = await extractTakesFromPages(engine, { bootstrapEnabled: true });
|
||||
expect(keyless.llm_unavailable).toBe(true);
|
||||
expect(keyless.claims_extracted).toBe(0);
|
||||
const beforeRows = await engine.executeRaw<{ n: number }>(
|
||||
`SELECT COUNT(*)::int AS n FROM takes t JOIN pages p ON p.id = t.page_id WHERE p.slug = $1`,
|
||||
[SLUG],
|
||||
);
|
||||
expect(beforeRows[0].n).toBe(0);
|
||||
|
||||
// Keyed: configure the real chat model and run the same sweep.
|
||||
const chat = resolveChatProvider()!;
|
||||
configureGateway({ chat_model: chat.model, env: chat.env });
|
||||
const keyed = await extractTakesFromPages(engine, { bootstrapEnabled: true, holder: 'test' });
|
||||
expect(keyed.llm_unavailable).toBe(false);
|
||||
expect(keyed.claims_extracted).toBeGreaterThan(0);
|
||||
|
||||
// The claim physically landed in the takes table on OUR probe page.
|
||||
const rows = await engine.executeRaw<{ claim: string; kind: string }>(
|
||||
`SELECT t.claim, t.kind FROM takes t JOIN pages p ON p.id = t.page_id WHERE p.slug = $1`,
|
||||
[SLUG],
|
||||
);
|
||||
expect(rows.length).toBeGreaterThan(0);
|
||||
expect(rows.every((r) => r.claim.trim().length > 0)).toBe(true);
|
||||
expect(rows.every((r) => ['fact', 'take', 'bet', 'hunch'].includes(r.kind))).toBe(true);
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// (c) Postgres bootstrap: drive the verify core's roundtrip + graph_floor +
|
||||
// magic_moment probes against a REAL Postgres engine. First bootstrap
|
||||
// coverage off PGLite — catches PGLite-only assumptions in the probes
|
||||
// (tsvector recall, graph-edge materialization, probe cleanup DDL).
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
describe.skipIf(!DATABASE_URL)('Postgres bootstrap verify (real Postgres)', () => {
|
||||
let engine: PostgresEngine;
|
||||
let home: string;
|
||||
let root: string;
|
||||
let ws: string;
|
||||
let prevHome: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = mkdtempSync(join(tmpdir(), 'gb-pg-verify-'));
|
||||
home = join(root, '.gbrain');
|
||||
mkdirSync(join(home, 'bootstrap'), { recursive: true });
|
||||
writeFileSync(join(home, 'config.json'), JSON.stringify({ engine: 'postgres' }));
|
||||
ws = mkdtempSync(join(tmpdir(), 'gb-pg-verify-ws-'));
|
||||
mkdirSync(join(ws, 'brain'), { recursive: true });
|
||||
prevHome = process.env.GBRAIN_HOME;
|
||||
process.env.GBRAIN_HOME = root;
|
||||
|
||||
engine = new PostgresEngine();
|
||||
await engine.connect({ database_url: DATABASE_URL! });
|
||||
await engine.initSchema();
|
||||
await addSource(engine, { id: 'workspace', localPath: join(ws, 'brain'), force: true });
|
||||
}, 60_000);
|
||||
|
||||
afterAll(async () => {
|
||||
try { await engine.disconnect(); } catch { /* noop */ }
|
||||
if (prevHome === undefined) delete process.env.GBRAIN_HOME;
|
||||
else process.env.GBRAIN_HOME = prevHome;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
rmSync(ws, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('roundtrip + graph_floor + magic_moment pass on Postgres; probes cleaned up', async () => {
|
||||
const res = await verifyWorkspace(engine, ws, {
|
||||
sourceId: 'workspace',
|
||||
gbrainHomeDir: home,
|
||||
capabilities: KEYLESS,
|
||||
skipHooksSmoke: true,
|
||||
sweepBudgetMs: 20_000,
|
||||
});
|
||||
|
||||
// The three probes the task targets — each must pass on real Postgres.
|
||||
for (const c of res.checks.filter((c) => c.id === 'roundtrip')) expect(c.ok).toBe(true);
|
||||
const graph = res.checks.find((c) => c.id === 'graph_floor');
|
||||
expect(graph?.ok).toBe(true);
|
||||
const magic = res.checks.find((c) => c.id === 'magic_moment');
|
||||
expect(magic?.ok).toBe(true);
|
||||
|
||||
// Probe cleanup [G13] must hold on the Postgres DDL path too.
|
||||
const probePages = await engine.executeRaw<{ n: number }>(
|
||||
`SELECT COUNT(*)::int AS n FROM pages WHERE source_id = $1 AND slug IN ($2, $3)`,
|
||||
['workspace', VERIFY_PROBE_SLUG, VERIFY_PROBE_ENTITY_SLUG],
|
||||
);
|
||||
expect(probePages[0].n).toBe(0);
|
||||
const probeFacts = await engine.executeRaw<{ n: number }>(
|
||||
`SELECT COUNT(*)::int AS n FROM facts WHERE source_id = $1 AND source_markdown_slug = $2`,
|
||||
['workspace', VERIFY_PROBE_SLUG],
|
||||
);
|
||||
expect(probeFacts[0].n).toBe(0);
|
||||
// Sanity: the magic-token probe fact did not survive either.
|
||||
const tokenFacts = await engine.executeRaw<{ n: number }>(
|
||||
`SELECT COUNT(*)::int AS n FROM facts WHERE source_id = $1 AND source_markdown_slug = $2 AND fact LIKE $3`,
|
||||
['workspace', VERIFY_PROBE_SLUG, `%${VERIFY_MAGIC_TOKEN}%`],
|
||||
);
|
||||
expect(tokenFacts[0].n).toBe(0);
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -201,14 +201,23 @@ exit 0
|
||||
);
|
||||
chmodSync(join(shimDir, 'claude'), 0o755);
|
||||
|
||||
// STATEFUL codex shim: `mcp add` records the exact command+env line, `mcp
|
||||
// get` echoes it back, `mcp list`/`remove` reflect real state. This is what
|
||||
// lets verifyMcpTargetsWorkspace ([FIX7]) actually pass/fail — an inert shim
|
||||
// that always printed "gbrain" made the mismatch detector unfalsifiable.
|
||||
// State lives next to the argv record so it's cleaned up with shimDir.
|
||||
writeFileSync(
|
||||
join(shimDir, 'codex'),
|
||||
`#!/bin/sh
|
||||
echo "codex $*" >> "$GB_FAKE_RECORD"
|
||||
STATE="$(dirname "$GB_FAKE_RECORD")/codex-mcp.state"
|
||||
case "$1 $2" in
|
||||
"mcp add") exit 0 ;;
|
||||
"mcp list") echo "gbrain"; exit 0 ;;
|
||||
"mcp remove") exit 0 ;;
|
||||
"mcp add") printf '%s\\n' "$*" > "$STATE"; exit 0 ;;
|
||||
"mcp get")
|
||||
[ -f "$STATE" ] || { echo "no such MCP server: $3" >&2; exit 1; }
|
||||
cat "$STATE"; exit 0 ;;
|
||||
"mcp list") [ -f "$STATE" ] && echo "gbrain: stdio serve"; exit 0 ;;
|
||||
"mcp remove") rm -f "$STATE"; exit 0 ;;
|
||||
esac
|
||||
exit 0
|
||||
`,
|
||||
@@ -442,12 +451,18 @@ describe('bootstrap lifecycle (serial e2e)', () => {
|
||||
// binding [G1]); NO hooks written — the pull protocol is the per-turn
|
||||
// seam, stated plainly.
|
||||
const gbrainBin = join(shimDir, 'gbrain');
|
||||
expect(
|
||||
await runBootstrap(['hooks', '--workspace', ws2, '--harness', 'codex', '--gbrain-bin', gbrainBin], {
|
||||
const { result: hooksCode, out: hooksOut } = await captureStdout(() =>
|
||||
runBootstrap(['hooks', '--workspace', ws2, '--harness', 'codex', '--gbrain-bin', gbrainBin], {
|
||||
runner: testRunner,
|
||||
}),
|
||||
).toBe(0);
|
||||
);
|
||||
expect(hooksCode).toBe(0);
|
||||
expect(record()).toContain('codex mcp add gbrain --env GBRAIN_SOURCE=workspace');
|
||||
// [FIX7] the stateful shim echoes the recorded reg on `mcp get`, so the
|
||||
// registration smoke VERIFIES it targets THIS workspace (binary + source)
|
||||
// rather than falling back to the substring probe. A regressed
|
||||
// verifyMcpTargetsWorkspace (mismatch/unknown) would change this line.
|
||||
expect(hooksOut).toContain('MCP registered with codex (scope: user-global) — verified targeting this workspace.');
|
||||
expect(existsSync(join(ws2, '.claude', 'settings.local.json'))).toBe(false);
|
||||
const receipt = readReceipt(home);
|
||||
// Receipt is machine-scoped last-attach-wins: ws2's registration replaced ws's record.
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Cross-session "magic moment" — the keyless agent-authored memory contract
|
||||
* proven across a REAL engine session boundary (agent-bootstrap GAP 1).
|
||||
*
|
||||
* The shipped `gbrain bootstrap verify` magic_moment check writes a `## Facts`
|
||||
* fence, reconciles it in the SAME connection, and reads it straight back with
|
||||
* one SQL statement. That proves the fence parser runs — but it can NOT prove
|
||||
* the fact survives a process/connection boundary, that the ZERO-LLM sweep pass
|
||||
* actually banks it to disk, or that a keyless BM25 recall (the query op) finds
|
||||
* it afterward scoped to the right source. This test closes that gap:
|
||||
*
|
||||
* Session 1 (engine A): init a real on-disk PGLite brain + two sources,
|
||||
* author a fact through the REAL write path (put_page
|
||||
* op → `## Facts` fence), run the maintenance sweep's
|
||||
* zero-LLM fence pass, then DISCONNECT (engine A is
|
||||
* gone — the session boundary is real, not simulated).
|
||||
* Session 2 (engine B): a BRAND-NEW engine opens the SAME database_path with
|
||||
* NO initSchema (the data must already be on disk), and
|
||||
* we prove three things that the same-connection check
|
||||
* can't:
|
||||
* 1. persistence — the reconciled fact row survived
|
||||
* to disk and reopens in a fresh connection.
|
||||
* 2. keyless recall — the `query` op (BM25, no API
|
||||
* key) returns the fence page BY CONTENT.
|
||||
* 3. source-scope routing — the same recall scoped to
|
||||
* a DIFFERENT registered source returns nothing.
|
||||
*
|
||||
* Keyless is forced deterministically (every provider key stripped from the
|
||||
* env + a keyless capability report handed to the sweep) so the path under
|
||||
* test is exactly the fresh-install / keyless posture, and CI without secrets
|
||||
* runs it identically.
|
||||
*
|
||||
* Serial: cold PGLite init + a real disconnect/reopen cycle; env is
|
||||
* save/restored; temp dirs are cleaned no matter what.
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { createEngine } from '../../src/core/engine-factory.ts';
|
||||
import { addSource } from '../../src/core/sources-ops.ts';
|
||||
import { runMaintenanceSweep } from '../../src/core/sweep.ts';
|
||||
import type { CapabilityReport } from '../../src/core/capability.ts';
|
||||
import { operations, type Operation, type OperationContext } from '../../src/core/operations.ts';
|
||||
import type { BrainEngine } from '../../src/core/engine.ts';
|
||||
|
||||
// ── Fixture: a distinctive, wikilink-free fence fact ────────────────────────
|
||||
// The claim carries rare tokens so a keyless BM25 recall is unambiguous, and no
|
||||
// `|` so the fence-table markdown parses as a single claim cell.
|
||||
const PROBE_SLUG = 'wiki/magic-moment-probe';
|
||||
const MAGIC_PHRASE =
|
||||
'The migratory narwhal expedition logged forty-two beacons near the aurora fjord';
|
||||
const RECALL_QUERY = 'narwhal expedition beacons aurora fjord';
|
||||
|
||||
const PROBE_CONTENT = `---
|
||||
title: Magic Moment Probe
|
||||
type: concept
|
||||
---
|
||||
|
||||
# Magic Moment Probe
|
||||
|
||||
A keyless agent-authored memory probe for the cross-session magic-moment e2e.
|
||||
|
||||
## Facts
|
||||
|
||||
<!--- gbrain:facts:begin -->
|
||||
| # | claim | kind | confidence | visibility | notability | valid_from | valid_until | source | context |
|
||||
|---|-------|------|------------|------------|------------|------------|-------------|--------|---------|
|
||||
| 1 | ${MAGIC_PHRASE} | fact | 1.0 | world | low | | | magic-moment-test | |
|
||||
<!--- gbrain:facts:end -->
|
||||
`;
|
||||
|
||||
// Keyless posture handed to the sweep so the fence pass runs zero-LLM and the
|
||||
// corpus-ingest pass never reaches for a network key.
|
||||
const KEYLESS_CAPS: CapabilityReport = {
|
||||
embeddings: { available: false },
|
||||
extraction: { available: false },
|
||||
search: 'keyword-only',
|
||||
mode: 'keyless',
|
||||
};
|
||||
|
||||
const WORKSPACE_SOURCE = 'workspace';
|
||||
const OTHER_SOURCE = 'other';
|
||||
|
||||
// Every provider key that detectCapabilities / the sweep could observe. Stripped
|
||||
// so keyless is deterministic (and no accidental live embedding cost/flake).
|
||||
const PROVIDER_KEYS = [
|
||||
'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'ZEROENTROPY_API_KEY', 'OPENROUTER_API_KEY',
|
||||
'VOYAGE_API_KEY', 'DASHSCOPE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY',
|
||||
];
|
||||
const ENV_KEYS = [
|
||||
'GBRAIN_HOME', 'GBRAIN_DATABASE_URL', 'DATABASE_URL', 'GBRAIN_BRAIN_ID',
|
||||
'GBRAIN_SOURCE', ...PROVIDER_KEYS,
|
||||
];
|
||||
const SAVED_ENV: Record<string, string | undefined> = {};
|
||||
|
||||
let tmpParent: string;
|
||||
let home: string;
|
||||
let dbDir: string;
|
||||
let wsBrain: string;
|
||||
let otherBrain: string;
|
||||
let engineConfig: { engine: 'pglite'; database_path: string };
|
||||
let session2: BrainEngine;
|
||||
|
||||
function findOp(name: string): Operation {
|
||||
const op = operations.find((o) => o.name === name);
|
||||
if (!op) throw new Error(`op not registered: ${name}`);
|
||||
return op;
|
||||
}
|
||||
|
||||
/** Trusted-local ctx (remote:false) so put_page fires its post-hooks and the
|
||||
* query op runs the local hybrid path — mirrors the corpus helper's trustedCtx. */
|
||||
function trustedCtx(engine: BrainEngine, sourceId: string): OperationContext {
|
||||
return {
|
||||
engine,
|
||||
config: { engine: 'pglite', database_path: dbDir } as never,
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId,
|
||||
};
|
||||
}
|
||||
|
||||
async function factsMatching(engine: BrainEngine, sourceId: string): Promise<string[]> {
|
||||
const rows = await engine.executeRaw<{ fact: string }>(
|
||||
`SELECT fact FROM facts WHERE source_id = $1 AND visibility = 'world' AND fact LIKE $2`,
|
||||
[sourceId, `%${MAGIC_PHRASE}%`],
|
||||
);
|
||||
return rows.map((r) => r.fact);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
for (const k of ENV_KEYS) SAVED_ENV[k] = process.env[k];
|
||||
// Ambient-state strip: a dev/CI DATABASE_URL must not flip the sandboxed brain
|
||||
// to Postgres, and a real provider key must not turn the keyless path semantic.
|
||||
delete process.env.GBRAIN_DATABASE_URL;
|
||||
delete process.env.DATABASE_URL;
|
||||
delete process.env.GBRAIN_BRAIN_ID;
|
||||
delete process.env.GBRAIN_SOURCE;
|
||||
for (const k of PROVIDER_KEYS) delete process.env[k];
|
||||
|
||||
tmpParent = mkdtempSync(join(tmpdir(), 'gb-magic-'));
|
||||
home = join(tmpParent, '.gbrain');
|
||||
mkdirSync(home, { recursive: true });
|
||||
dbDir = join(tmpParent, 'db');
|
||||
wsBrain = join(tmpParent, 'ws', 'brain');
|
||||
otherBrain = join(tmpParent, 'other', 'brain');
|
||||
mkdirSync(wsBrain, { recursive: true });
|
||||
mkdirSync(otherBrain, { recursive: true });
|
||||
process.env.GBRAIN_HOME = tmpParent;
|
||||
|
||||
writeFileSync(
|
||||
join(home, 'config.json'),
|
||||
JSON.stringify({ engine: 'pglite', database_path: dbDir, embedding_dimensions: 1536 }, null, 2),
|
||||
);
|
||||
engineConfig = { engine: 'pglite', database_path: dbDir };
|
||||
|
||||
// ── SESSION 1 (engine A): author + reconcile + DISCONNECT ────────────────
|
||||
const engineA = await createEngine(engineConfig);
|
||||
await engineA.connect(engineConfig);
|
||||
await engineA.initSchema();
|
||||
await addSource(engineA, { id: WORKSPACE_SOURCE, localPath: wsBrain, force: true });
|
||||
await addSource(engineA, { id: OTHER_SOURCE, localPath: otherBrain, force: true });
|
||||
|
||||
// Author through the REAL write path: put_page op (remote:false) with a
|
||||
// `## Facts` fence — the keyless agent-authored memory route.
|
||||
const putPage = findOp('put_page');
|
||||
await putPage.handler(trustedCtx(engineA, WORKSPACE_SOURCE), {
|
||||
slug: PROBE_SLUG,
|
||||
content: PROBE_CONTENT,
|
||||
});
|
||||
|
||||
// Reconcile the fence into the facts index via the sweep's ZERO-LLM pass —
|
||||
// the exact `gbrain sweep --once` fence path, keyless.
|
||||
const report = await runMaintenanceSweep(engineA, {
|
||||
sourceId: WORKSPACE_SOURCE,
|
||||
budgetMs: 60_000,
|
||||
capabilities: KEYLESS_CAPS,
|
||||
});
|
||||
// Session-1 sanity: the fence really reconciled in THIS connection.
|
||||
if (report.factsReconciled < 1) {
|
||||
throw new Error(
|
||||
`session 1 fence reconciliation failed (factsReconciled=${report.factsReconciled}, ` +
|
||||
`skipped=${JSON.stringify(report.skipped)}) — cannot test cross-session recall`,
|
||||
);
|
||||
}
|
||||
|
||||
// The session boundary: engine A is fully torn down. Session 2 opens a fresh
|
||||
// engine against the same on-disk directory.
|
||||
await engineA.disconnect();
|
||||
|
||||
// ── SESSION 2 (engine B): fresh engine, SAME db, NO initSchema ───────────
|
||||
session2 = await createEngine(engineConfig);
|
||||
await session2.connect(engineConfig);
|
||||
}, 180_000);
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await session2?.disconnect();
|
||||
} catch {
|
||||
/* already down */
|
||||
}
|
||||
for (const k of ENV_KEYS) {
|
||||
if (SAVED_ENV[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = SAVED_ENV[k];
|
||||
}
|
||||
try {
|
||||
rmSync(tmpParent, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
});
|
||||
|
||||
describe('bootstrap magic moment — cross-session keyless recall (serial e2e) [GAP1]', () => {
|
||||
test('persistence: the fence-reconciled fact survives to disk and reopens in a fresh engine', async () => {
|
||||
// engine A wrote + disconnected; engine B never ran initSchema. If the
|
||||
// reconciled row didn't bank to disk, this comes back empty.
|
||||
const facts = await factsMatching(session2, WORKSPACE_SOURCE);
|
||||
expect(facts.length).toBeGreaterThanOrEqual(1);
|
||||
expect(facts[0]).toContain(MAGIC_PHRASE);
|
||||
}, 60_000);
|
||||
|
||||
test('keyless recall: the query op (BM25) returns the fence page BY CONTENT across the session boundary', async () => {
|
||||
const queryOp = findOp('query');
|
||||
const results = (await queryOp.handler(trustedCtx(session2, WORKSPACE_SOURCE), {
|
||||
query: RECALL_QUERY,
|
||||
limit: 10,
|
||||
expand: false,
|
||||
source_id: WORKSPACE_SOURCE,
|
||||
})) as Array<{ slug: string; chunk_text: string }>;
|
||||
|
||||
// Real content assertion: the fence page comes back AND the recalled chunk
|
||||
// carries the authored fact text — not merely "some non-empty result".
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
const hit = results.find((r) => r.slug === PROBE_SLUG);
|
||||
expect(hit).toBeDefined();
|
||||
expect(JSON.stringify(results)).toContain(MAGIC_PHRASE);
|
||||
}, 60_000);
|
||||
|
||||
test('source-scope routing: the same recall scoped to a DIFFERENT source returns neither the fact nor the page', async () => {
|
||||
// The fact and page live in `workspace`; `other` is registered but empty.
|
||||
// A cross-session query scoped to `other` must not leak workspace content.
|
||||
const otherFacts = await factsMatching(session2, OTHER_SOURCE);
|
||||
expect(otherFacts.length).toBe(0);
|
||||
|
||||
const queryOp = findOp('query');
|
||||
const results = (await queryOp.handler(trustedCtx(session2, OTHER_SOURCE), {
|
||||
query: RECALL_QUERY,
|
||||
limit: 10,
|
||||
expand: false,
|
||||
source_id: OTHER_SOURCE,
|
||||
})) as Array<{ slug: string; chunk_text: string }>;
|
||||
expect(results.find((r) => r.slug === PROBE_SLUG)).toBeUndefined();
|
||||
expect(JSON.stringify(results)).not.toContain(MAGIC_PHRASE);
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* GAP 7 — session-end persistence chain, UNMOCKED end-to-end.
|
||||
*
|
||||
* `test/hook-command.serial.test.ts` proves session-end up to the mocked
|
||||
* `io.spawnPush` seam (it only records that a push WOULD fire). This file
|
||||
* closes the remaining gap: it drives the session-end hook against a REAL
|
||||
* local bare git remote with the REAL push implementation — the same
|
||||
* `workspacePush` the detached `gbrain sources push` child runs — so the full
|
||||
* hook → scan → commit → push chain is exercised and its outcome verified on
|
||||
* the remote.
|
||||
*
|
||||
* The only concession to a hermetic test is `allowUnverifiedRemote: true`:
|
||||
* `workspacePush`'s privacy gate can only confirm PRIVATE visibility for a
|
||||
* real GitHub origin via `gh`, which a `file://` bare remote isn't — every
|
||||
* sibling `workspace-push.serial.test.ts` case makes the same concession. The
|
||||
* scan/commit/push machinery under test is otherwise identical to production.
|
||||
*
|
||||
* Serial: mutates HOME/GBRAIN_HOME + spawns git subprocesses. All secrets are
|
||||
* synthetic fixtures; GBRAIN_GIT_ALLOW_FILE_TRANSPORT=1 permits file transport.
|
||||
*/
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
import { runHook } from '../../src/commands/hook.ts';
|
||||
import { workspacePush, type WorkspacePushResult } from '../../src/core/workspace-push.ts';
|
||||
import { writeManifest } from '../../src/core/bootstrap/format.ts';
|
||||
|
||||
const OPENAI_KEY = 'sk-' + 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4';
|
||||
const ENV_KEYS = ['HOME', 'GBRAIN_HOME', 'DATABASE_URL', 'GBRAIN_DATABASE_URL', 'GBRAIN_HOOKS', 'GBRAIN_GIT_ALLOW_FILE_TRANSPORT'] as const;
|
||||
|
||||
// #2943: env: process.env is REQUIRED — Bun snapshots env at startup, so a
|
||||
// spawned git would otherwise be blind to beforeEach's HOME/GBRAIN_HOME swap.
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
return execFileSync('git', ['-C', cwd, '-c', 'protocol.file.allow=always', ...args], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', env: process.env,
|
||||
}).trim();
|
||||
}
|
||||
function originHead(bare: string): string {
|
||||
return git(bare, 'rev-parse', 'refs/heads/main');
|
||||
}
|
||||
|
||||
const MANIFEST = {
|
||||
format_version: 1 as const,
|
||||
initialized: true as const,
|
||||
agent_name: 'persist-test',
|
||||
created_by: 'test',
|
||||
created_at: '2026-01-01T00:00:00.000Z',
|
||||
source_id: 'workspace',
|
||||
};
|
||||
|
||||
let root: string;
|
||||
let bare: string;
|
||||
let work: string;
|
||||
let saved: Record<string, string | undefined>;
|
||||
|
||||
/** Real push path, in-process: exactly what `gbrain sources push --path`
|
||||
* invokes. Captured so the test can await the chain the hook kicks off. */
|
||||
let pushes: Promise<WorkspacePushResult>[];
|
||||
const realSpawnPush = (r: string) => {
|
||||
pushes.push(workspacePush({ dir: r, branch: 'main', allowUnverifiedRemote: true }));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), 'gb-persist-'));
|
||||
saved = {};
|
||||
for (const k of ENV_KEYS) saved[k] = process.env[k];
|
||||
for (const k of ['DATABASE_URL', 'GBRAIN_DATABASE_URL', 'GBRAIN_HOOKS'] as const) delete process.env[k];
|
||||
process.env.HOME = mkdtempSync(join(root, 'home-'));
|
||||
// CX2-8: GBRAIN_HOME is a PARENT dir → effective home is $HOME/.gbrain.
|
||||
process.env.GBRAIN_HOME = process.env.HOME;
|
||||
process.env.GBRAIN_GIT_ALLOW_FILE_TRANSPORT = '1';
|
||||
|
||||
bare = mkdtempSync(join(root, 'origin-')) + '.git';
|
||||
execFileSync('git', ['init', '-q', '--bare', '-b', 'main', bare], { stdio: 'ignore', env: process.env });
|
||||
work = mkdtempSync(join(root, 'work-'));
|
||||
execFileSync('git', ['-c', 'protocol.file.allow=always', 'clone', '-q', bare, work], { stdio: 'ignore', env: process.env });
|
||||
git(work, 'config', 'user.email', 't@t.t');
|
||||
git(work, 'config', 'user.name', 'tester');
|
||||
writeFileSync(join(work, 'README.md'), 'init\n');
|
||||
git(work, 'add', 'README.md');
|
||||
git(work, 'commit', '-qm', 'init');
|
||||
git(work, 'push', '-q', 'origin', 'main');
|
||||
try { git(work, 'remote', 'set-head', 'origin', 'main'); } catch { /* */ }
|
||||
|
||||
// The initialized manifest is the security boundary the hook gates on.
|
||||
writeManifest(work, MANIFEST);
|
||||
pushes = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const k of ENV_KEYS) {
|
||||
if (saved[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = saved[k];
|
||||
}
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('session-end → real workspace push', () => {
|
||||
test('authored session content actually lands on the bare remote', async () => {
|
||||
// The agent authored real work into the brain during the session.
|
||||
mkdirSync(join(work, 'brain'), { recursive: true });
|
||||
const authored = 'the acme-example roadmap decision: ship the memory layer in Q3';
|
||||
writeFileSync(join(work, 'brain', 'decision.md'), `# decision\n\n${authored}\n`);
|
||||
|
||||
const before = originHead(bare);
|
||||
const code = await runHook(['session-end'], {
|
||||
write: () => {},
|
||||
cwd: work,
|
||||
spawnPush: realSpawnPush,
|
||||
stdin: JSON.stringify({ session_id: 'persist-ok', cwd: work }),
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
|
||||
// The hook kicked off exactly one real push; await its completion.
|
||||
expect(pushes).toHaveLength(1);
|
||||
const res = await pushes[0];
|
||||
expect(res.status).toBe('pushed');
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.committed).toBe(true);
|
||||
|
||||
// Origin advanced, and the authored file+content are physically there.
|
||||
expect(originHead(bare)).not.toBe(before);
|
||||
const verify = mkdtempSync(join(root, 'verify-'));
|
||||
execFileSync('git', ['-c', 'protocol.file.allow=always', 'clone', '-q', bare, verify], { stdio: 'ignore', env: process.env });
|
||||
const landed = join(verify, 'brain', 'decision.md');
|
||||
expect(existsSync(landed)).toBe(true);
|
||||
expect(readFileSync(landed, 'utf-8')).toContain(authored);
|
||||
}, 60_000);
|
||||
|
||||
test('a planted secret in authored content is BLOCKED at the push gate — nothing leaves', async () => {
|
||||
mkdirSync(join(work, 'brain'), { recursive: true });
|
||||
// A genuine note plus a file that leaks a synthetic OpenAI key.
|
||||
writeFileSync(join(work, 'brain', 'safe.md'), '# safe\n\nno secrets here\n');
|
||||
writeFileSync(join(work, 'brain', 'leak.md'), `# oops\n\napi key: ${OPENAI_KEY}\n`);
|
||||
|
||||
const before = originHead(bare);
|
||||
const code = await runHook(['session-end'], {
|
||||
write: () => {},
|
||||
cwd: work,
|
||||
spawnPush: realSpawnPush,
|
||||
stdin: JSON.stringify({ session_id: 'persist-leak', cwd: work }),
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
|
||||
expect(pushes).toHaveLength(1);
|
||||
const res = await pushes[0];
|
||||
// The scan gate fired: nothing committed, nothing pushed, secret named.
|
||||
expect(res.status).toBe('blocked_secrets');
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.findings?.some((f) => f.file === 'brain/leak.md' && f.pattern === 'openai')).toBe(true);
|
||||
// The secret VALUE never surfaces in the result payload.
|
||||
expect(JSON.stringify(res).includes(OPENAI_KEY)).toBe(false);
|
||||
|
||||
// Origin is untouched — the leak never left the machine.
|
||||
expect(originHead(bare)).toBe(before);
|
||||
const shipped = git(bare, 'ls-tree', '-r', '--name-only', 'main');
|
||||
expect(shipped).not.toContain('brain/leak.md');
|
||||
expect(shipped).not.toContain('brain/safe.md'); // blocked atomically — nothing in the batch shipped
|
||||
}, 60_000);
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# bootstrap-corpus fixture
|
||||
|
||||
A tiny, hermetic, 100% synthetic brain corpus used by the agent-bootstrap end-to-end
|
||||
tests. It is curated and re-authored (not copied wholesale) from the public gbrain-evals
|
||||
synthetic corpora — `eval/data/synthetic-v1` (interlinked markdown pages with frontmatter,
|
||||
`[[wikilinks]]`, and `## Timeline` sections), `eval/data/world-v1` (compiled-truth entity
|
||||
JSON), and `eval/precisionmembench/fixtures` (preference/belief objects with visibility +
|
||||
confidence). Every name here is an obvious placeholder (`alice-example`, `ridge-platform`,
|
||||
`summit-robotics`, `vector-co`, …); no real person, company, or fund appears, per the repo
|
||||
privacy iron rule.
|
||||
|
||||
Contents:
|
||||
|
||||
- `pages/*.md` — 12 interlinked pages (people, companies, concepts, meetings) with YAML
|
||||
frontmatter and `[[wikilinks]]`. Filenames flatten the slug: `companies__ridge-platform.md`
|
||||
loads as slug `companies/ridge-platform`. Four pages carry a `## Timeline` section.
|
||||
- `beliefs.json` — belief/fact objects `{ text, entity_slug, visibility, confidence }`, a
|
||||
mix of `world` and `private`, so a visibility-fence recall test can assert the private
|
||||
ones are never surfaced.
|
||||
- `queries.json` — gold recall cases `{ id, kind, query, expect_slug?, expect_substring?,
|
||||
must_not_substring? }`, each with a deterministic answer given the pages/beliefs above.
|
||||
|
||||
The loader in `test/helpers/bootstrap-corpus.ts` reads these files and writes the pages
|
||||
through the real `put_page` operation handler (so auto-link, chunking, and search-vector
|
||||
fire) and the beliefs through `engine.insertFact` (honoring each belief's visibility).
|
||||
`loadCorpusQueries()` returns the parsed gold cases. Consume the loader read-only; do not
|
||||
mutate the fixtures from a test.
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"text": "alice-example prefers async written standups over live meetings.",
|
||||
"entity_slug": "people/alice-example",
|
||||
"visibility": "world",
|
||||
"confidence": 0.9
|
||||
},
|
||||
{
|
||||
"text": "ridge-platform ships PGLite as its default embedded engine for new installs.",
|
||||
"entity_slug": "companies/ridge-platform",
|
||||
"visibility": "world",
|
||||
"confidence": 0.95
|
||||
},
|
||||
{
|
||||
"text": "carol-fictional owns the hybrid-retrieval reranker and reviews all changes to it.",
|
||||
"entity_slug": "people/carol-fictional",
|
||||
"visibility": "world",
|
||||
"confidence": 0.85
|
||||
},
|
||||
{
|
||||
"text": "summit-robotics uses graph-traversal for warehouse path planning.",
|
||||
"entity_slug": "companies/summit-robotics",
|
||||
"visibility": "world",
|
||||
"confidence": 0.9
|
||||
},
|
||||
{
|
||||
"text": "bob-placeholder invests through vector-co and mentors alice-example.",
|
||||
"entity_slug": "people/bob-placeholder",
|
||||
"visibility": "world",
|
||||
"confidence": 0.8
|
||||
},
|
||||
{
|
||||
"text": "vector-co focuses its fund on memory-systems and robotics startups.",
|
||||
"entity_slug": "companies/vector-co",
|
||||
"visibility": "world",
|
||||
"confidence": 0.75
|
||||
},
|
||||
{
|
||||
"text": "synthesis-layers reduces token budget by pre-fetching a synopsis before recall.",
|
||||
"entity_slug": "concepts/synthesis-layers",
|
||||
"visibility": "world",
|
||||
"confidence": 0.7
|
||||
},
|
||||
{
|
||||
"text": "alice-example privately plans to step down as CEO of ridge-platform next year.",
|
||||
"entity_slug": "people/alice-example",
|
||||
"visibility": "private",
|
||||
"confidence": 0.6
|
||||
},
|
||||
{
|
||||
"text": "summit-robotics is quietly exploring an acquisition offer from a larger competitor.",
|
||||
"entity_slug": "companies/summit-robotics",
|
||||
"visibility": "private",
|
||||
"confidence": 0.5
|
||||
},
|
||||
{
|
||||
"text": "carol-fictional is unhappy with her equity grant and considering leaving ridge-platform.",
|
||||
"entity_slug": "people/carol-fictional",
|
||||
"visibility": "private",
|
||||
"confidence": 0.55
|
||||
},
|
||||
{
|
||||
"text": "vector-co marked down its ridge-platform stake in the latest internal report.",
|
||||
"entity_slug": "companies/vector-co",
|
||||
"visibility": "private",
|
||||
"confidence": 0.5
|
||||
},
|
||||
{
|
||||
"text": "bob-placeholder is negotiating a secret side letter with an undisclosed limited partner.",
|
||||
"entity_slug": "people/bob-placeholder",
|
||||
"visibility": "private",
|
||||
"confidence": 0.5
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type: company
|
||||
title: Ridge Platform
|
||||
aliases: [ridge, ridge-platform-inc]
|
||||
---
|
||||
|
||||
# Ridge Platform
|
||||
|
||||
A memory-systems company building agent retrieval infrastructure. Founded 2024.
|
||||
|
||||
Led by CEO [[people/alice-example]] with staff engineer [[people/carol-fictional]].
|
||||
Ridge Platform's core research area is [[concepts/agent-memory]], and its flagship
|
||||
retrieval stack is built on [[concepts/hybrid-retrieval]] and
|
||||
[[concepts/synthesis-layers]]. Backed by investor [[companies/vector-co]].
|
||||
|
||||
## Timeline
|
||||
|
||||
- **2024-04-02** | incorporated as an eval-frameworks startup
|
||||
- **2025-07-19** | launched the hosted [[concepts/hybrid-retrieval]] service
|
||||
- **2026-02-10** | raised a growth round from [[companies/vector-co]]
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type: company
|
||||
title: Summit Robotics
|
||||
aliases: [summit]
|
||||
---
|
||||
|
||||
# Summit Robotics
|
||||
|
||||
A robotics startup building warehouse navigation robots. Founded 2024 by
|
||||
[[people/bob-placeholder]].
|
||||
|
||||
Summit Robotics relies on [[concepts/graph-traversal]] for path planning and
|
||||
partners with [[companies/ridge-platform]] on memory-systems research. Funded by
|
||||
[[companies/vector-co]].
|
||||
|
||||
## Timeline
|
||||
|
||||
- **2024-02-10** | founded by [[people/bob-placeholder]]
|
||||
- **2024-11-01** | shipped the first [[concepts/graph-traversal]] navigation demo
|
||||
- **2025-08-25** | signed a research partnership with [[companies/ridge-platform]]
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
type: company
|
||||
title: Vector Co
|
||||
aliases: [vector, vector-capital]
|
||||
---
|
||||
|
||||
# Vector Co
|
||||
|
||||
An early-stage investment fund focused on memory-systems and robotics.
|
||||
|
||||
Vector Co backs [[companies/ridge-platform]] and [[companies/summit-robotics]], and
|
||||
partner [[people/bob-placeholder]] sits on both boards.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
type: concept
|
||||
title: agent memory
|
||||
aliases: [agent-memory]
|
||||
---
|
||||
|
||||
# agent memory
|
||||
|
||||
The discipline of persisting and recalling facts across agent sessions.
|
||||
|
||||
Core research area at [[companies/ridge-platform]]. Underpins
|
||||
[[concepts/synthesis-layers]] and is retrieved through [[concepts/hybrid-retrieval]].
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
type: concept
|
||||
title: graph traversal
|
||||
aliases: [graph-traversal]
|
||||
---
|
||||
|
||||
# graph traversal
|
||||
|
||||
A path-planning technique that walks a typed edge graph to find routes.
|
||||
|
||||
Used in production at [[companies/summit-robotics]] and championed by
|
||||
[[people/bob-placeholder]]. Related to [[concepts/agent-memory]] graph recall.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
type: concept
|
||||
title: hybrid retrieval
|
||||
aliases: [hybrid-retrieval]
|
||||
---
|
||||
|
||||
# hybrid retrieval
|
||||
|
||||
A retrieval method that fuses dense embeddings with sparse keyword search.
|
||||
|
||||
Built by [[people/carol-fictional]] at [[companies/ridge-platform]]. Feeds
|
||||
[[concepts/synthesis-layers]] and complements [[concepts/agent-memory]].
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
type: concept
|
||||
title: synthesis layers
|
||||
---
|
||||
|
||||
# synthesis layers
|
||||
|
||||
A pattern in memory-systems that composes retrieved chunks into higher-order
|
||||
summaries before they reach the agent.
|
||||
|
||||
Deployed at [[companies/ridge-platform]] and studied by [[people/alice-example]].
|
||||
Compare to [[concepts/hybrid-retrieval]] and builds on [[concepts/agent-memory]].
|
||||
|
||||
## Notes
|
||||
|
||||
Synthesis layers reduce token budget by pre-fetching a synopsis. Reference work in
|
||||
[[concepts/hybrid-retrieval]] demonstrates the retrieval side of the same idea.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
---
|
||||
type: meeting
|
||||
title: graph traversal review
|
||||
date: 2026-02-21
|
||||
---
|
||||
|
||||
# graph traversal review — 2026-02-21
|
||||
|
||||
Attendees: [[people/bob-placeholder]], [[people/alice-example]].
|
||||
|
||||
## Notes
|
||||
|
||||
Walked through [[concepts/graph-traversal]] benchmarks for
|
||||
[[companies/summit-robotics]]. Bob proposed a joint study with
|
||||
[[companies/ridge-platform]] on memory-systems. Investor [[companies/vector-co]]
|
||||
asked for a follow-up deck.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
type: meeting
|
||||
title: memory systems sync
|
||||
date: 2026-03-16
|
||||
---
|
||||
|
||||
# memory systems sync — 2026-03-16
|
||||
|
||||
Attendees: [[people/alice-example]], [[people/carol-fictional]].
|
||||
|
||||
## Notes
|
||||
|
||||
Reviewed [[concepts/synthesis-layers]] rollout at [[companies/ridge-platform]].
|
||||
Carol reported that [[concepts/hybrid-retrieval]] recall improved after the latest
|
||||
reranker change. Next steps coordinated via [[people/alice-example]].
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
type: person
|
||||
title: Alice Example
|
||||
aliases: [alice, a-founder]
|
||||
---
|
||||
|
||||
# Alice Example
|
||||
|
||||
CEO at [[companies/ridge-platform]]. Previously an engineer at [[companies/summit-robotics]].
|
||||
|
||||
Works on memory-systems and [[concepts/synthesis-layers]]. Alice drives the
|
||||
retrieval-quality agenda together with [[people/carol-fictional]] and reports the
|
||||
company roadmap to investor [[companies/vector-co]].
|
||||
|
||||
## Timeline
|
||||
|
||||
- **2024-06-05** | joined [[companies/ridge-platform]] as founding engineer
|
||||
- **2025-02-18** | shipped the first [[concepts/hybrid-retrieval]] prototype
|
||||
- **2026-01-26** | promoted to CEO of [[companies/ridge-platform]]
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
type: person
|
||||
title: Bob Placeholder
|
||||
aliases: [bob]
|
||||
---
|
||||
|
||||
# Bob Placeholder
|
||||
|
||||
Founder and CEO of [[companies/summit-robotics]]. Angel investor via
|
||||
[[companies/vector-co]].
|
||||
|
||||
Bob mentors [[people/alice-example]] and champions [[concepts/graph-traversal]] as
|
||||
the backbone of Summit's navigation stack.
|
||||
|
||||
## Timeline
|
||||
|
||||
- **2024-02-10** | founded [[companies/summit-robotics]]
|
||||
- **2025-05-30** | closed the seed round with [[companies/vector-co]]
|
||||
- **2026-03-04** | opened the second robotics lab
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
type: person
|
||||
title: Carol Fictional
|
||||
aliases: [carol]
|
||||
---
|
||||
|
||||
# Carol Fictional
|
||||
|
||||
Staff engineer at [[companies/ridge-platform]], working alongside
|
||||
[[people/alice-example]].
|
||||
|
||||
Carol owns the [[concepts/hybrid-retrieval]] pipeline and researches
|
||||
[[concepts/agent-memory]] retention strategies.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"id": "page-recall-summit-warehouse",
|
||||
"kind": "page",
|
||||
"query": "warehouse navigation robots startup",
|
||||
"expect_slug": "companies/summit-robotics",
|
||||
"expect_substring": "warehouse"
|
||||
},
|
||||
{
|
||||
"id": "page-recall-graph-traversal",
|
||||
"kind": "page",
|
||||
"query": "graph traversal path planning technique",
|
||||
"expect_slug": "concepts/graph-traversal",
|
||||
"expect_substring": "path-planning"
|
||||
},
|
||||
{
|
||||
"id": "page-recall-hybrid-retrieval",
|
||||
"kind": "page",
|
||||
"query": "hybrid retrieval dense embeddings sparse keyword",
|
||||
"expect_slug": "concepts/hybrid-retrieval",
|
||||
"expect_substring": "dense embeddings"
|
||||
},
|
||||
{
|
||||
"id": "page-recall-alice-join",
|
||||
"kind": "page",
|
||||
"query": "when did alice-example join ridge-platform",
|
||||
"expect_slug": "people/alice-example",
|
||||
"expect_substring": "founding engineer"
|
||||
},
|
||||
{
|
||||
"id": "belief-recall-alice-standups",
|
||||
"kind": "belief",
|
||||
"query": "how does alice-example prefer to run standups",
|
||||
"expect_substring": "async",
|
||||
"must_not_substring": "step down"
|
||||
},
|
||||
{
|
||||
"id": "belief-recall-ridge-engine",
|
||||
"kind": "belief",
|
||||
"query": "what engine does ridge-platform ship by default",
|
||||
"expect_substring": "PGLite"
|
||||
},
|
||||
{
|
||||
"id": "private-fence-alice-plans",
|
||||
"kind": "fence",
|
||||
"query": "what are alice-example private future plans",
|
||||
"must_not_substring": "step down"
|
||||
},
|
||||
{
|
||||
"id": "private-fence-summit-acquisition",
|
||||
"kind": "fence",
|
||||
"query": "is summit-robotics being acquired by a competitor",
|
||||
"must_not_substring": "acquisition"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Read-only loaders for the hermetic synthetic brain corpus vendored at
|
||||
* test/fixtures/bootstrap-corpus/. Consumed by the agent-bootstrap end-to-end
|
||||
* tests so they exercise real recall/graph behavior against a small, 100%
|
||||
* synthetic, privacy-safe world instead of depending on the sibling
|
||||
* gbrain-evals checkout (which may be absent in CI).
|
||||
*
|
||||
* Design notes for consumers:
|
||||
* - loadCorpusPages writes through the REAL `put_page` operation handler
|
||||
* with a trusted-local OperationContext (remote: false). That is what
|
||||
* fires auto-link (wikilink → graph edge), chunking, and search-vector
|
||||
* population — bare engine.putPage would skip those post-hooks. Pages are
|
||||
* loaded in TWO passes: pass 1 creates every row; pass 2 re-runs the
|
||||
* idempotent auto-link reconciler now that every wikilink target exists
|
||||
* (runAutoLink filters candidates to slugs present in getAllSlugs, so a
|
||||
* forward reference in a cyclic graph only resolves once its target row
|
||||
* is present). Returns the loaded slugs.
|
||||
* - loadCorpusBeliefs inserts each belief via engine.insertFact honoring its
|
||||
* declared visibility ('world' | 'private') so a visibility-fence recall
|
||||
* test can assert the private ones never surface. Returns the count.
|
||||
* - loadCorpusQueries returns the parsed gold cases (no engine needed).
|
||||
*
|
||||
* The loaders never mutate process.env; env sandboxing (GBRAIN_HOME /
|
||||
* DATABASE_URL stripping) is the caller's responsibility.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { operations } from '../../src/core/operations.ts';
|
||||
import type { OperationContext, Operation } from '../../src/core/operations.ts';
|
||||
import type { BrainEngine } from '../../src/core/engine.ts';
|
||||
|
||||
/** Absolute path to the vendored fixture directory. */
|
||||
export const CORPUS_DIR = join(import.meta.dir, '..', 'fixtures', 'bootstrap-corpus');
|
||||
const PAGES_DIR = join(CORPUS_DIR, 'pages');
|
||||
|
||||
/** A belief/fact object as stored in beliefs.json. */
|
||||
export interface CorpusBelief {
|
||||
text: string;
|
||||
entity_slug: string;
|
||||
visibility: 'world' | 'private';
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
/** A gold recall case as stored in queries.json. */
|
||||
export interface CorpusQuery {
|
||||
id: string;
|
||||
kind: 'page' | 'belief' | 'fence';
|
||||
query: string;
|
||||
expect_slug?: string;
|
||||
expect_substring?: string;
|
||||
must_not_substring?: string;
|
||||
}
|
||||
|
||||
const put_page = operations.find((o) => o.name === 'put_page') as Operation | undefined;
|
||||
if (!put_page) throw new Error('bootstrap-corpus loader: put_page op not registered');
|
||||
|
||||
/**
|
||||
* Build a trusted-local OperationContext (remote: false) so the put_page
|
||||
* handler honors auto-link / auto-timeline post-hooks. Mirrors the makeCtx
|
||||
* pattern in test/source-id-tx-regression.test.ts.
|
||||
*/
|
||||
function trustedCtx(engine: BrainEngine, sourceId: string): OperationContext {
|
||||
return {
|
||||
engine,
|
||||
config: { engine: 'pglite' } as never,
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId,
|
||||
};
|
||||
}
|
||||
|
||||
/** Filename `people__alice-example.md` → slug `people/alice-example`. */
|
||||
function fileToSlug(filename: string): string {
|
||||
return filename.replace(/\.md$/, '').replace(/__/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the corpus markdown pages and write them through the put_page op
|
||||
* handler (remote: false) so auto-link, chunking, and search-vector fire.
|
||||
* Two passes: pass 1 seeds all rows, pass 2 lets the auto-link reconciler
|
||||
* resolve every [[wikilink]] now that all targets exist. Returns the slugs.
|
||||
*/
|
||||
export async function loadCorpusPages(
|
||||
engine: BrainEngine,
|
||||
opts: { sourceId?: string } = {},
|
||||
): Promise<string[]> {
|
||||
const sourceId = opts.sourceId ?? 'default';
|
||||
const ctx = trustedCtx(engine, sourceId);
|
||||
const files = readdirSync(PAGES_DIR)
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.sort();
|
||||
const docs = files.map((f) => ({
|
||||
slug: fileToSlug(f),
|
||||
content: readFileSync(join(PAGES_DIR, f), 'utf8'),
|
||||
}));
|
||||
|
||||
// Pass 1: create every page row (some wikilink targets may not exist yet).
|
||||
// Pass 2: re-run — auto-link reconciles now that every target row exists.
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
for (const doc of docs) {
|
||||
await put_page!.handler(ctx, { slug: doc.slug, content: doc.content });
|
||||
}
|
||||
}
|
||||
return docs.map((d) => d.slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the corpus beliefs via engine.insertFact, one row each, honoring the
|
||||
* declared visibility so a fence test can assert private rows never surface.
|
||||
* Returns the number of rows inserted.
|
||||
*/
|
||||
export async function loadCorpusBeliefs(
|
||||
engine: BrainEngine,
|
||||
opts: { sourceId?: string } = {},
|
||||
): Promise<number> {
|
||||
const sourceId = opts.sourceId ?? 'default';
|
||||
const beliefs = loadCorpusBeliefData();
|
||||
let inserted = 0;
|
||||
for (const b of beliefs) {
|
||||
await engine.insertFact(
|
||||
{
|
||||
fact: b.text,
|
||||
kind: 'fact',
|
||||
entity_slug: b.entity_slug,
|
||||
visibility: b.visibility,
|
||||
confidence: b.confidence,
|
||||
source: 'test:bootstrap-corpus',
|
||||
embedding: null,
|
||||
},
|
||||
{ source_id: sourceId },
|
||||
);
|
||||
inserted++;
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
|
||||
/** Parse and return the raw belief objects (no engine needed). */
|
||||
export function loadCorpusBeliefData(): CorpusBelief[] {
|
||||
return JSON.parse(readFileSync(join(CORPUS_DIR, 'beliefs.json'), 'utf8')) as CorpusBelief[];
|
||||
}
|
||||
|
||||
/** Parse and return the gold recall cases (no engine needed). */
|
||||
export function loadCorpusQueries(): CorpusQuery[] {
|
||||
return JSON.parse(readFileSync(join(CORPUS_DIR, 'queries.json'), 'utf8')) as CorpusQuery[];
|
||||
}
|
||||
@@ -22,6 +22,13 @@ import { buildOperationContext } from '../src/mcp/dispatch.ts';
|
||||
import type { OperationContext } from '../src/core/operations.ts';
|
||||
import type { GBrainConfig } from '../src/core/config.ts';
|
||||
import type { BrainEngine } from '../src/core/engine.ts';
|
||||
import type { WindowTurn } from '../src/core/context/entity-salience.ts';
|
||||
import {
|
||||
loadCorpusPages,
|
||||
loadCorpusBeliefs,
|
||||
loadCorpusBeliefData,
|
||||
loadCorpusQueries,
|
||||
} from './helpers/bootstrap-corpus.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
|
||||
@@ -239,3 +246,92 @@ describe('dispatch typed session identity [CX2-11]', () => {
|
||||
expect(buildOperationContext(fakeEngine, { _meta: { session_id: '' } }, { sourceId: 'default' }).sessionId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('corpus recall on a multi-entity brain [S3#1 fence, real recall]', () => {
|
||||
// Deterministic proper-case surface for a slug tail so the zero-LLM,
|
||||
// proper-case-biased entity resolver fires on a page-kind query.
|
||||
// 'concepts/graph-traversal' → 'Graph Traversal'
|
||||
const nameFromSlug = (slug: string): string =>
|
||||
slug
|
||||
.split('/')
|
||||
.pop()!
|
||||
.split('-')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
|
||||
test('every gold query: world answer surfaces, private beliefs are fenced, named pages resolve to pointers', async () => {
|
||||
// beforeEach cleared pages/facts/aliases. Seed the WHOLE synthetic world
|
||||
// through the real handlers: put_page fires auto-link + search vector,
|
||||
// insertFact honors each belief's declared visibility.
|
||||
const slugs = await loadCorpusPages(engine, { sourceId: 'default' });
|
||||
expect(slugs.length).toBeGreaterThanOrEqual(12);
|
||||
const inserted = await loadCorpusBeliefs(engine, { sourceId: 'default' });
|
||||
expect(inserted).toBe(loadCorpusBeliefData().length);
|
||||
// Beliefs were inserted after any prior meta read this test — start the
|
||||
// hot-memory cache clean so the world/private tiers are (re)computed.
|
||||
__resetHotMemoryCacheForTests();
|
||||
|
||||
const privateTexts = loadCorpusBeliefData()
|
||||
.filter((b) => b.visibility === 'private')
|
||||
.map((b) => b.text);
|
||||
expect(privateTexts.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const queries = loadCorpusQueries();
|
||||
expect(queries.length).toBeGreaterThan(0);
|
||||
|
||||
// Collect every violation with its gold-case id so a regression names the
|
||||
// exact query + assertion that broke, instead of a bare boolean.
|
||||
const violations: string[] = [];
|
||||
for (const q of queries) {
|
||||
// Page-kind: name the entity so the reflex resolver runs against the real
|
||||
// 12-page graph. belief / fence: the raw query — only the world/private
|
||||
// hot-memory fence decides what surfaces.
|
||||
const window: WindowTurn[] =
|
||||
q.kind === 'page' && q.expect_slug
|
||||
? [{ role: 'user', text: `the latest on ${nameFromSlug(q.expect_slug)}` }]
|
||||
: [{ role: 'user', text: q.query }];
|
||||
const r = await assembleTurnContext(engine, { sourceId: 'default', window });
|
||||
|
||||
// Fence (S3#1): NO private belief text ever crosses into the block,
|
||||
// regardless of what the user asked.
|
||||
for (const pt of privateTexts) {
|
||||
if (r.text.includes(pt)) violations.push(`${q.id}: leaked private belief "${pt.slice(0, 40)}…"`);
|
||||
}
|
||||
if (q.must_not_substring && r.text.includes(q.must_not_substring)) {
|
||||
violations.push(`${q.id}: fenced substring surfaced "${q.must_not_substring}"`);
|
||||
}
|
||||
if (q.kind === 'belief' && q.expect_substring && !r.text.includes(q.expect_substring)) {
|
||||
violations.push(`${q.id}: world belief not recalled "${q.expect_substring}"`);
|
||||
}
|
||||
if (q.kind === 'page' && q.expect_slug) {
|
||||
if (!r.pointers.some((p) => p.slug === q.expect_slug)) {
|
||||
violations.push(`${q.id}: entity did not resolve to a pointer (${q.expect_slug})`);
|
||||
}
|
||||
if (!r.text.includes(q.expect_slug)) {
|
||||
violations.push(`${q.id}: expected slug missing from the block (${q.expect_slug})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(violations).toEqual([]);
|
||||
}, 120_000);
|
||||
|
||||
test('reflex pointer synopsis carries real page-body content, not a generic stub', async () => {
|
||||
await loadCorpusPages(engine, { sourceId: 'default' });
|
||||
// Named entity → pointer, and the pointer's synopsis is drawn from THAT
|
||||
// page's body (the world-fenced first prose line), so distinctive body
|
||||
// content reaches the assembled block.
|
||||
const cases: Array<[name: string, slug: string, needle: string]> = [
|
||||
['Summit Robotics', 'companies/summit-robotics', 'warehouse'],
|
||||
['Graph Traversal', 'concepts/graph-traversal', 'path-planning'],
|
||||
['Hybrid Retrieval', 'concepts/hybrid-retrieval', 'dense embeddings'],
|
||||
];
|
||||
for (const [name, slug, needle] of cases) {
|
||||
const r = await assembleTurnContext(engine, {
|
||||
sourceId: 'default',
|
||||
window: [{ role: 'user', text: `context on ${name}` }],
|
||||
});
|
||||
expect(r.pointers.map((p) => p.slug)).toContain(slug);
|
||||
expect(r.text).toContain(needle);
|
||||
}
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
@@ -103,6 +103,58 @@ EOF
|
||||
chmod +x "$SCRATCH/bin/gh"
|
||||
export PATH="$SCRATCH/bin:$PATH"
|
||||
|
||||
# ── Fake codex shim (stateful MCP registry, offline) ────────────────────────
|
||||
# The container has no real codex, but the registration step must still be
|
||||
# EXERCISED (not skipped). `mcp add` records the exact command+env line; `mcp
|
||||
# get` echoes it back so verifyMcpTargetsWorkspace ([FIX7]) can genuinely
|
||||
# confirm the serve targets this workspace (binary path + GBRAIN_SOURCE).
|
||||
# State path rides an exported env var so it survives the bun->codex spawn.
|
||||
step "fake codex shim"
|
||||
export GB_CODEX_STATE="$SCRATCH/codex-mcp.state"
|
||||
cat > "$SCRATCH/bin/codex" <<'EOF'
|
||||
#!/bin/sh
|
||||
STATE="${GB_CODEX_STATE:-/tmp/codex-mcp.state}"
|
||||
case "$1 $2" in
|
||||
--version*) echo "codex 0.0.0 (offline-fake)"; exit 0 ;;
|
||||
"mcp add") printf '%s\n' "$*" > "$STATE"; exit 0 ;;
|
||||
"mcp get")
|
||||
[ -f "$STATE" ] || { echo "no such MCP server: $3" >&2; exit 1; }
|
||||
cat "$STATE"; exit 0 ;;
|
||||
"mcp list") [ -f "$STATE" ] && echo "gbrain: stdio serve"; exit 0 ;;
|
||||
"mcp remove") rm -f "$STATE"; exit 0 ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$SCRATCH/bin/codex"
|
||||
# Absolute gbrain binary for the registration argv (never executed — the codex
|
||||
# path writes no hooks and the MCP add is faked).
|
||||
FAKE_GBRAIN="$SCRATCH/bin/gbrain-bin"
|
||||
printf '#!/bin/sh\nexit 0\n' > "$FAKE_GBRAIN"
|
||||
chmod +x "$FAKE_GBRAIN"
|
||||
|
||||
# ── Fake git push shim (offline) ────────────────────────────────────────────
|
||||
# The fake gh adds an https origin, so repo.ts's real `git push` would try to
|
||||
# reach github.com — impossible under --network none (and it prompts for
|
||||
# credentials when network IS allowed for the local smoke). Intercept `push`
|
||||
# (absorbed — nothing leaves the container) and `ls-remote` (reports the branch
|
||||
# exists) exactly like the in-repo lifecycle e2e's git shim; delegate every
|
||||
# other subcommand (init/add/commit/remote/rev-parse) to real git. Capture the
|
||||
# real git path BEFORE this shim shadows it on PATH.
|
||||
REAL_GIT="$(command -v git)"
|
||||
[ -n "$REAL_GIT" ] || fail "git not on PATH — the e2e needs a real git to delegate to"
|
||||
cat > "$SCRATCH/bin/git" <<EOF
|
||||
#!/bin/sh
|
||||
for a in "\$@"; do
|
||||
if [ "\$a" = "push" ]; then exit 0; fi
|
||||
if [ "\$a" = "ls-remote" ]; then
|
||||
echo "0000000000000000000000000000000000000000 refs/heads/main"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exec "$REAL_GIT" "\$@"
|
||||
EOF
|
||||
chmod +x "$SCRATCH/bin/git"
|
||||
|
||||
# ── Engine phase artifact: keyless PGLite config ─────────────────────────────
|
||||
step "engine config"
|
||||
cat > "$GBRAIN_HOME/.gbrain/config.json" <<EOF
|
||||
@@ -177,5 +229,18 @@ grep -Fq '"ok": true' "$SCRATCH/verify.json" || fail "verify did not pass"
|
||||
grep -Fq '"mode": "keyless"' "$SCRATCH/verify.json" || fail "capability report is not keyless"
|
||||
grep -Fq 'smoke not applicable' "$SCRATCH/verify.json" || fail "hooks_smoke degradation not named"
|
||||
|
||||
# ── Codex door: exercise MCP registration through the fake stateful codex ────
|
||||
# No real codex binary, but the argv/registration step MUST run (not skip). The
|
||||
# stateful shim lets the [FIX7] smoke actually verify the serve targets this
|
||||
# workspace; the codex path writes NO .claude hooks (pull protocol covers it).
|
||||
step "codex MCP registration (fake stateful codex)"
|
||||
codex_out="$(gbrain bootstrap hooks --workspace "$WS" --harness codex --gbrain-bin "$FAKE_GBRAIN" 2>&1)"
|
||||
printf '%s\n' "$codex_out"
|
||||
printf '%s\n' "$codex_out" | grep -Fq "verified targeting this workspace" \
|
||||
|| fail "codex MCP registration was not verified as targeting this workspace"
|
||||
[ -f "$WS/.claude/settings.local.json" ] && fail "codex path must not write Claude hooks"
|
||||
grep -Fq "GBRAIN_SOURCE=" "$GB_CODEX_STATE" || fail "codex registration did not bind GBRAIN_SOURCE"
|
||||
grep -Fq "serve --surface full" "$GB_CODEX_STATE" || fail "codex registration did not pin the full op surface"
|
||||
|
||||
echo
|
||||
echo "PASS: offline bootstrap e2e (interview -> render -> repo -> abort/resume -> keyless verify)"
|
||||
echo "PASS: offline bootstrap e2e (interview -> render -> repo -> abort/resume -> keyless verify -> codex MCP)"
|
||||
|
||||
Reference in New Issue
Block a user