mirror of https://github.com/garrytan/gstack.git
test(brain): real PGLite round-trip E2E (matched-pair persistence)
test/skill-e2e-gbrain-roundtrip-local.test.ts (~145 LOC, periodic-tier, ~$0.001/run on Voyage): Real gbrain CLI round-trip against an isolated temp HOME: 1. gbrain init --pglite --embedding-model voyage:voyage-code-3 2. gbrain put office-hours/<unique-slug> --content <markdown> 3. gbrain get <slug> 4. Assert every body line survives + title + tags + non-empty This is the matched-pair check for the v1.50.0.0 question "is the data we hope to save actually being saved?" — proves the gbrain CLI persistence contract gstack relies on, against a real engine. Does NOT involve the agent — pure CLI integration test. The agent obedience side is covered by the fake-CLI E2E in the prior commit. Skips cleanly when VOYAGE_API_KEY is unset OR gbrain CLI is missing from PATH, so CI without secrets degrades gracefully. Remote/Supabase routing is gbrain's contract — the same CLI shape works against every engine. gstack stops at local round-trip coverage to avoid re-testing gbrain's MCP client implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
33b016712a
commit
d9c5b15e77
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* E2E: real gbrain CLI round-trip against a local PGLite engine.
|
||||
*
|
||||
* Replaces the manual local probe documented in earlier drafts of
|
||||
* docs/gbrain-write-surfaces.md. The matched-pair check the user asked
|
||||
* for v1.50.0.0: "is the data we hope to save actually being saved?"
|
||||
*
|
||||
* What this proves:
|
||||
* - The gbrain CLI subcommand shape gstack ships (`gbrain put <slug>
|
||||
* --content "<markdown with frontmatter>"`) actually persists to a
|
||||
* real PGLite store.
|
||||
* - The page is retrievable via `gbrain get <slug>` with body + title
|
||||
* intact (frontmatter is allowed to be reformatted by gbrain — we
|
||||
* check semantic fields, not byte-exact YAML).
|
||||
* - The `office-hours/<slug>` slug namespace works (no rejection,
|
||||
* no auto-rewrite).
|
||||
*
|
||||
* What this does NOT prove (out of scope, owned elsewhere):
|
||||
* - Agent obedience to the resolver instructions — that's the
|
||||
* fake-CLI E2E (test/skill-e2e-office-hours-brain-writeback.test.ts).
|
||||
* - Remote-MCP persistence — that's the write-shape E2E
|
||||
* (test/skill-e2e-gbrain-roundtrip-remote.test.ts).
|
||||
* - gbrain's own internal correctness — gbrain has its own test suite;
|
||||
* this is a contract smoke test, not gbrain validation.
|
||||
*
|
||||
* Periodic tier. Real gbrain init + put triggers one Voyage embedding
|
||||
* call (~$0.001/run). Skips when VOYAGE_API_KEY is unset OR gbrain is
|
||||
* not on PATH, so CI without secrets degrades gracefully.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
import {
|
||||
describeIfSelected,
|
||||
testConcurrentIfSelected,
|
||||
runId,
|
||||
createEvalCollector,
|
||||
} from './helpers/e2e-helpers';
|
||||
|
||||
const evalCollector = createEvalCollector('e2e-gbrain-roundtrip-local');
|
||||
|
||||
function gbrainOnPath(): boolean {
|
||||
try {
|
||||
execFileSync('gbrain', ['--version'], { stdio: 'pipe', timeout: 5_000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const SHOULD_RUN_GUARDS_OK =
|
||||
gbrainOnPath() && !!process.env.VOYAGE_API_KEY;
|
||||
|
||||
describeIfSelected(
|
||||
'GBrain local PGLite round-trip E2E',
|
||||
['gbrain-roundtrip-local'],
|
||||
() => {
|
||||
let tmpHome: string;
|
||||
const slug = `office-hours/roundtrip-test-${Date.now()}`;
|
||||
const body = `# Roundtrip test
|
||||
|
||||
This is a deterministic round-trip test page used by the gstack v1.50.0.0
|
||||
brain-writeback verification. Generated at ${new Date().toISOString()}.
|
||||
|
||||
If gbrain persisted this correctly, you should see this exact body when
|
||||
you run \`gbrain get "${slug}"\`.`;
|
||||
|
||||
beforeAll(() => {
|
||||
if (!SHOULD_RUN_GUARDS_OK) {
|
||||
// Will skip via testConcurrentIfSelected gate; nothing to set up.
|
||||
tmpHome = '';
|
||||
return;
|
||||
}
|
||||
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-roundtrip-'));
|
||||
|
||||
// Initialize a real PGLite gbrain in the isolated temp HOME. Explicit
|
||||
// --embedding-model required because the local env has multiple
|
||||
// providers ready (voyage + zeroentropyai); gbrain refuses to guess.
|
||||
execFileSync(
|
||||
'gbrain',
|
||||
['init', '--pglite', '--embedding-model', 'voyage:voyage-code-3'],
|
||||
{
|
||||
env: { ...process.env, HOME: tmpHome },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 60_000,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (tmpHome) {
|
||||
try {
|
||||
rmSync(tmpHome, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testConcurrentIfSelected(
|
||||
'gbrain-roundtrip-local',
|
||||
async () => {
|
||||
if (!SHOULD_RUN_GUARDS_OK) {
|
||||
console.log(
|
||||
'[skip] gbrain CLI not on PATH or VOYAGE_API_KEY unset; ' +
|
||||
'this E2E proves the gbrain CLI persistence contract gstack relies on. ' +
|
||||
'Run locally with `VOYAGE_API_KEY=... bun test ...` to verify before shipping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = `---
|
||||
title: "Office Hours: Roundtrip Test"
|
||||
tags: [design-doc, roundtrip-test]
|
||||
---
|
||||
${body}`;
|
||||
|
||||
// PUT the page.
|
||||
execFileSync('gbrain', ['put', slug, '--content', content], {
|
||||
env: { ...process.env, HOME: tmpHome },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
// GET it back.
|
||||
const retrieved = execFileSync('gbrain', ['get', slug], {
|
||||
env: { ...process.env, HOME: tmpHome },
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// The body MUST survive verbatim — every line of what we wrote
|
||||
// must appear in what we got back. (Frontmatter reformatting is
|
||||
// gbrain's prerogative; body text is data we own.)
|
||||
for (const line of body.split('\n')) {
|
||||
if (line.trim()) {
|
||||
expect(retrieved).toContain(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Title is in the frontmatter — assert it's present (gbrain
|
||||
// strips the constant prefix "title: " quote handling can vary).
|
||||
expect(retrieved).toContain('Roundtrip Test');
|
||||
|
||||
// Tag survived.
|
||||
expect(retrieved).toContain('design-doc');
|
||||
expect(retrieved).toContain('roundtrip-test');
|
||||
|
||||
// Sanity: the doc isn't empty or a 404 error.
|
||||
expect(retrieved.length).toBeGreaterThan(body.length);
|
||||
expect(retrieved).not.toContain('page_not_found');
|
||||
expect(retrieved).not.toContain('Page not found');
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue