Merge remote-tracking branch 'origin/main' into garrytan/columbus-v2

# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	VERSION
#	docs/skills.md
#	package.json
This commit is contained in:
Garry Tan
2026-05-14 18:38:13 -07:00
22 changed files with 2588 additions and 243 deletions
+246
View File
@@ -0,0 +1,246 @@
/**
* Shape regression test for bin/gstack-gbrain-detect.
*
* After the bash→TS rewrite (codex #5), the TS output must stay
* key/type/semantics backward-compatible with the bash version. Downstream
* callers across most gstack skill preambles shell out to this script and
* pipe through jq. Key order may differ between bash+jq and JSON.stringify;
* key NAMES and TYPES must not.
*
* Asserts:
* 1. All 9 pre-existing keys are present
* 2. Each pre-existing key has the same primitive type/union as the bash version
* 3. The new key (gbrain_local_status) is present and a string
* 4. Output is parseable JSON
* 5. No keys removed/renamed
*/
import { describe, it, expect } from "bun:test";
import { execFileSync } from "child_process";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
chmodSync,
rmSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
const DETECT_BIN = join(import.meta.dir, "..", "bin", "gstack-gbrain-detect");
/** Absolute bun path resolved once at module load (uses the test runner's PATH). */
const BUN_BIN = execFileSync("sh", ["-c", "command -v bun"], { encoding: "utf-8" }).trim();
/**
* Run detect with a controlled HOME + PATH so the output is deterministic.
* We invoke via `bun run <path>` instead of the shebang so the test doesn't
* need bun on its PATH. The script's child-process probes still respect
* the controlled PATH.
*/
function runDetect(env: Partial<NodeJS.ProcessEnv>): string {
return execFileSync(BUN_BIN, ["run", DETECT_BIN], {
encoding: "utf-8",
timeout: 15_000,
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, ...env },
});
}
interface DetectShape {
gbrain_on_path: boolean;
gbrain_version: string | null;
gbrain_config_exists: boolean;
gbrain_engine: string | null;
gbrain_doctor_ok: boolean;
gbrain_mcp_mode: string;
gstack_brain_sync_mode: string;
gstack_brain_git: boolean;
gstack_artifacts_remote: string;
gbrain_local_status: string;
}
describe("bin/gstack-gbrain-detect — shape regression", () => {
it("emits valid JSON", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin",
GSTACK_HOME: tmp,
});
expect(() => JSON.parse(out)).not.toThrow();
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("contains all 9 pre-existing keys + the new gbrain_local_status key", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin",
GSTACK_HOME: tmp,
});
const parsed = JSON.parse(out) as DetectShape;
// 9 pre-existing keys (must not be removed/renamed):
expect(parsed).toHaveProperty("gbrain_on_path");
expect(parsed).toHaveProperty("gbrain_version");
expect(parsed).toHaveProperty("gbrain_config_exists");
expect(parsed).toHaveProperty("gbrain_engine");
expect(parsed).toHaveProperty("gbrain_doctor_ok");
expect(parsed).toHaveProperty("gbrain_mcp_mode");
expect(parsed).toHaveProperty("gstack_brain_sync_mode");
expect(parsed).toHaveProperty("gstack_brain_git");
expect(parsed).toHaveProperty("gstack_artifacts_remote");
// 1 new key (added by this fix):
expect(parsed).toHaveProperty("gbrain_local_status");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("preserves field types from the bash version", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin",
GSTACK_HOME: tmp,
});
const parsed = JSON.parse(out) as Record<string, unknown>;
// Booleans (bash: `true`/`false`; TS: boolean)
expect(typeof parsed.gbrain_on_path).toBe("boolean");
expect(typeof parsed.gbrain_config_exists).toBe("boolean");
expect(typeof parsed.gbrain_doctor_ok).toBe("boolean");
expect(typeof parsed.gstack_brain_git).toBe("boolean");
// String | null unions (bash: `null` when absent; TS: null when absent)
const versionType = parsed.gbrain_version === null ? "null" : typeof parsed.gbrain_version;
expect(versionType === "string" || versionType === "null").toBe(true);
const engineType = parsed.gbrain_engine === null ? "null" : typeof parsed.gbrain_engine;
expect(engineType === "string" || engineType === "null").toBe(true);
// Strings (bash: always emits a string, never null)
expect(typeof parsed.gbrain_mcp_mode).toBe("string");
expect(typeof parsed.gstack_brain_sync_mode).toBe("string");
expect(typeof parsed.gstack_artifacts_remote).toBe("string");
// New field: string enum
expect(typeof parsed.gbrain_local_status).toBe("string");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("gbrain_mcp_mode is one of the three documented values", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin",
GSTACK_HOME: tmp,
});
const parsed = JSON.parse(out) as DetectShape;
expect(["local-stdio", "remote-http", "none"]).toContain(parsed.gbrain_mcp_mode);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("gstack_brain_sync_mode is one of the three documented values", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin",
GSTACK_HOME: tmp,
});
const parsed = JSON.parse(out) as DetectShape;
expect(["off", "artifacts-only", "full"]).toContain(parsed.gstack_brain_sync_mode);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("gbrain_local_status is one of the five documented values", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin",
GSTACK_HOME: tmp,
});
const parsed = JSON.parse(out) as DetectShape;
expect(["ok", "no-cli", "missing-config", "broken-config", "broken-db"]).toContain(
parsed.gbrain_local_status,
);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("with no gbrain on PATH, returns gbrain_on_path=false and gbrain_local_status=no-cli", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
try {
const out = runDetect({
HOME: tmp,
PATH: "/usr/bin:/bin", // no gbrain on this PATH
GSTACK_HOME: tmp,
GSTACK_DETECT_NO_CACHE: "1",
});
const parsed = JSON.parse(out) as DetectShape;
expect(parsed.gbrain_on_path).toBe(false);
expect(parsed.gbrain_version).toBeNull();
expect(parsed.gbrain_local_status).toBe("no-cli");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("with fake gbrain that returns valid JSON, returns gbrain_on_path=true and gbrain_local_status=ok", () => {
const tmp = mkdtempSync(join(tmpdir(), "detect-shape-"));
const bindir = join(tmp, "bin");
const home = join(tmp, "home");
const configDir = join(home, ".gbrain");
const configPath = join(configDir, "config.json");
try {
mkdirSync(bindir, { recursive: true });
mkdirSync(home, { recursive: true });
mkdirSync(configDir, { recursive: true });
writeFileSync(configPath, JSON.stringify({ engine: "pglite" }));
// Fake gbrain: prints valid sources-list JSON
const fake = `#!/bin/sh
case "$1 $2" in
"--version ") echo "gbrain 0.33.1.0"; exit 0 ;;
"sources list") echo '{"sources":[]}'; exit 0 ;;
"doctor "*) echo '{"status":"ok","checks":[]}'; exit 0 ;;
esac
exit 0
`;
const gbrainPath = join(bindir, "gbrain");
writeFileSync(gbrainPath, fake);
chmodSync(gbrainPath, 0o755);
const out = runDetect({
HOME: home,
PATH: `${bindir}:/usr/bin:/bin`,
GSTACK_HOME: tmp,
GSTACK_DETECT_NO_CACHE: "1",
});
const parsed = JSON.parse(out) as DetectShape;
expect(parsed.gbrain_on_path).toBe(true);
expect(parsed.gbrain_version).toBe("gbrain0.33.1.0");
expect(parsed.gbrain_config_exists).toBe(true);
expect(parsed.gbrain_engine).toBe("pglite");
expect(parsed.gbrain_local_status).toBe("ok");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
});
+204
View File
@@ -0,0 +1,204 @@
/**
* Tests the .bak-rollback contract used by /setup-gbrain Step 1.5 (broken-db
* repair) and Step 4.5 (Path 4 opt-in to local PGLite), per plan D7.
*
* These code paths live in the skill TEMPLATE, not in a TypeScript helper —
* the skill follows AI-readable instructions. The instructions specify the
* exact sequence:
*
* 1. mv ~/.gbrain/config.json ~/.gbrain/config.json.gstack-bak-$(date +%s)
* 2. gbrain init --pglite --json
* 3. on non-zero exit: mv .bak back; surface error
*
* This test extracts that sequence as a shell function and verifies the
* rollback contract using a fake `gbrain` binary that fails on init. It's
* the test that proves "what the skill template says, when followed
* mechanically, actually preserves the user's broken config on failure."
*
* Per plan codex #10 / explicit rollback scope: we only promise to restore
* the config.json file. The PGLite directory at ~/.gbrain/pglite/ may end
* up in a partial state — that's documented to the user, not auto-cleaned.
*/
import { describe, it, expect } from "bun:test";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
readFileSync,
existsSync,
readdirSync,
rmSync,
chmodSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { spawnSync } from "child_process";
interface RollbackEnv {
tmp: string;
home: string;
configPath: string;
bindir: string;
cleanup: () => void;
}
function makeEnv(opts: { gbrainBehavior: "succeeds" | "fails" }): RollbackEnv {
const tmp = mkdtempSync(join(tmpdir(), "gbrain-init-rollback-"));
const home = join(tmp, "home");
const gbrainDir = join(home, ".gbrain");
const configPath = join(gbrainDir, "config.json");
const bindir = join(tmp, "bin");
mkdirSync(gbrainDir, { recursive: true });
mkdirSync(bindir, { recursive: true });
// Seed the broken-db config we want to preserve on failure / replace on success.
writeFileSync(
configPath,
JSON.stringify({
engine: "postgres",
database_url: "postgresql://stale:test@localhost:5435/gbrain_test",
}),
);
const exitCode = opts.gbrainBehavior === "fails" ? 1 : 0;
const onInitSuccess =
opts.gbrainBehavior === "succeeds"
? `cat > "${configPath}" <<JSON
{"engine":"pglite","database_url":"pglite://${gbrainDir}/pglite"}
JSON
mkdir -p "${gbrainDir}/pglite"
echo '{"status":"ok"}'`
: `echo "Error: disk full" >&2`;
const fake = `#!/bin/sh
if [ "$1" = "--version" ]; then echo "gbrain 0.33.1.0"; exit 0; fi
if [ "$1 $2" = "init --pglite" ]; then
${onInitSuccess}
exit ${exitCode}
fi
exit 0
`;
writeFileSync(join(bindir, "gbrain"), fake);
chmodSync(join(bindir, "gbrain"), 0o755);
return {
tmp,
home,
configPath,
bindir,
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
};
}
/**
* Verbatim reimplementation of the skill template's Step 1.5 / 4.5 rollback
* sequence. The skill instructs the model to execute this bash; we execute
* the same bash here in a sandboxed environment and assert the contract.
*
* If gbrain templates rewrite this sequence, this test should fail until
* the shell here is updated too. That's the point — keep the test and the
* skill template aligned.
*/
function runRollbackSequence(env: RollbackEnv): { exitCode: number; stderr: string } {
const script = `
set -u
BACKUP="${env.configPath}.gstack-bak-$(date +%s)-$$"
if [ -f "${env.configPath}" ]; then
mv "${env.configPath}" "$BACKUP"
fi
if ! gbrain init --pglite --json; then
if [ -n "\${BACKUP:-}" ] && [ -f "$BACKUP" ]; then
mv "$BACKUP" "${env.configPath}"
fi
echo "gbrain init failed. Existing config (if any) was restored." >&2
exit 1
fi
echo "ok"
`;
const result = spawnSync("bash", ["-c", script], {
encoding: "utf-8",
env: {
...process.env,
HOME: env.home,
PATH: `${env.bindir}:/usr/bin:/bin`,
},
});
return {
exitCode: result.status ?? 1,
stderr: result.stderr || "",
};
}
describe("Step 1.5 / 4.5 .bak-rollback contract (plan D7)", () => {
it("FAILURE PATH: when `gbrain init` fails, broken config is restored to original path", () => {
const env = makeEnv({ gbrainBehavior: "fails" });
try {
const originalContent = readFileSync(env.configPath, "utf-8");
const r = runRollbackSequence(env);
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain("restored");
// Original config is back at the original path.
expect(existsSync(env.configPath)).toBe(true);
const after = readFileSync(env.configPath, "utf-8");
expect(after).toBe(originalContent);
// No leftover .bak — it was renamed back to the original path.
const baks = readdirSync(join(env.home, ".gbrain")).filter((f) =>
f.includes(".gstack-bak-"),
);
expect(baks).toEqual([]);
} finally {
env.cleanup();
}
});
it("SUCCESS PATH: when `gbrain init` succeeds, the .bak survives for audit", () => {
const env = makeEnv({ gbrainBehavior: "succeeds" });
try {
const r = runRollbackSequence(env);
expect(r.exitCode).toBe(0);
// New config is in place (fake gbrain wrote pglite engine).
expect(existsSync(env.configPath)).toBe(true);
const after = JSON.parse(readFileSync(env.configPath, "utf-8")) as {
engine: string;
};
expect(after.engine).toBe("pglite");
// The .bak survives — user can audit before deleting.
const baks = readdirSync(join(env.home, ".gbrain")).filter((f) =>
f.includes(".gstack-bak-"),
);
expect(baks.length).toBe(1);
} finally {
env.cleanup();
}
});
it("PGLite directory partial state is NOT auto-cleaned (codex #10 scoped rollback)", () => {
// Per the rollback scope: we only restore config.json. If gbrain init
// started writing a PGLite dir before failing, we leave it alone and
// surface the cleanup hint to the user.
const env = makeEnv({ gbrainBehavior: "fails" });
try {
// Simulate gbrain having created a partial PGLite dir before failure
const partial = join(env.home, ".gbrain", "pglite");
mkdirSync(partial, { recursive: true });
writeFileSync(join(partial, "partial-write.tmp"), "");
const r = runRollbackSequence(env);
expect(r.exitCode).toBe(1);
// The partial dir is left in place — user gets the hint, we don't
// assume responsibility for cleanup.
expect(existsSync(partial)).toBe(true);
expect(existsSync(join(partial, "partial-write.tmp"))).toBe(true);
} finally {
env.cleanup();
}
});
});
+288
View File
@@ -0,0 +1,288 @@
/**
* Unit tests for lib/gbrain-local-status.ts.
*
* Per the eng-review D6 (gate-tier = mocked, codex #9): no real gbrain CLI, no
* real PGLite, no real Postgres. Each case builds a fake `gbrain` shell script
* on PATH that emits canned exit codes + stderr matching the patterns the
* classifier looks for.
*
* Five status cases:
* 1. no-cli — gbrain absent from PATH
* 2. missing-config — gbrain present, ~/.gbrain/config.json absent
* 3. broken-config — gbrain present, config exists, stderr contains "config.json"
* 4. broken-db — gbrain present, config exists, stderr contains "Cannot connect to database"
* 5. ok — gbrain present, config exists, sources list returns valid JSON
*
* Plus cache behavior: hit, TTL expiry, invariant invalidation (HOME change),
* --no-cache bypass.
*/
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import {
mkdtempSync,
writeFileSync,
mkdirSync,
rmSync,
chmodSync,
existsSync,
utimesSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
import {
localEngineStatus,
cacheFilePath,
CACHE_TTL_MS,
type LocalEngineStatus,
} from "../lib/gbrain-local-status";
interface FakeEnv {
tmp: string;
bindir: string;
home: string;
gstackHome: string;
configPath: string;
cleanup: () => void;
}
/**
* Build a tmp HOME + GSTACK_HOME + optional fake `gbrain` on PATH.
*
* The classifier reads HOME via os.homedir() which reads process.env.HOME, so
* we mutate process.env ambiently in each test (restored in afterEach).
*/
function makeEnv(opts: {
withGbrain?: boolean;
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "throws";
withConfig?: boolean;
}): FakeEnv {
const tmp = mkdtempSync(join(tmpdir(), "gbrain-local-status-test-"));
const bindir = join(tmp, "bin");
const home = join(tmp, "home");
const gstackHome = join(home, ".gstack");
const configDir = join(home, ".gbrain");
const configPath = join(configDir, "config.json");
mkdirSync(bindir, { recursive: true });
mkdirSync(home, { recursive: true });
mkdirSync(gstackHome, { recursive: true });
mkdirSync(configDir, { recursive: true });
if (opts.withConfig) {
writeFileSync(
configPath,
JSON.stringify({ engine: "pglite", database_url: "pglite:///fake" }),
);
}
if (opts.withGbrain) {
const behavior = opts.gbrainBehavior || "ok";
const fake = makeFakeGbrainScript(behavior);
const gbrainPath = join(bindir, "gbrain");
writeFileSync(gbrainPath, fake);
chmodSync(gbrainPath, 0o755);
}
return {
tmp,
bindir,
home,
gstackHome,
configPath,
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
};
}
function makeFakeGbrainScript(
behavior: "ok" | "broken-db" | "broken-config" | "throws",
): string {
const stderrLine =
behavior === "broken-db"
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
: behavior === "broken-config"
? 'echo "Error: malformed config.json at ~/.gbrain/config.json" >&2'
: behavior === "throws"
? 'echo "unexpected gbrain failure" >&2'
: "";
const exitCode = behavior === "ok" ? 0 : 1;
return `#!/bin/sh
if [ "$1" = "--version" ]; then
echo "gbrain 0.33.1.0"
exit 0
fi
if [ "$1 $2" = "sources list" ]; then
if [ ${exitCode} -eq 0 ]; then
echo '{"sources":[]}'
exit 0
fi
${stderrLine}
exit ${exitCode}
fi
exit 0
`;
}
/**
* Apply a FakeEnv to process.env. Returns a function that restores previous values.
*
* PATH is REPLACED (not prepended) so a real `gbrain` on the inherited PATH
* can't shadow the test's fake-or-absent binary. /usr/bin:/bin is kept so `sh`
* and `command` work.
*/
function applyEnv(env: FakeEnv): () => void {
const prev = {
HOME: process.env.HOME,
PATH: process.env.PATH,
GSTACK_HOME: process.env.GSTACK_HOME,
};
process.env.HOME = env.home;
process.env.PATH = `${env.bindir}:/usr/bin:/bin`;
process.env.GSTACK_HOME = env.gstackHome;
return () => {
if (prev.HOME === undefined) delete process.env.HOME;
else process.env.HOME = prev.HOME;
if (prev.PATH === undefined) delete process.env.PATH;
else process.env.PATH = prev.PATH;
if (prev.GSTACK_HOME === undefined) delete process.env.GSTACK_HOME;
else process.env.GSTACK_HOME = prev.GSTACK_HOME;
};
}
describe("lib/gbrain-local-status — five status cases", () => {
let env: FakeEnv | null = null;
let restoreEnv: (() => void) | null = null;
afterEach(() => {
if (restoreEnv) restoreEnv();
if (env) env.cleanup();
env = null;
restoreEnv = null;
});
it("returns 'no-cli' when gbrain is not on PATH", () => {
env = makeEnv({ withGbrain: false });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: true })).toBe("no-cli");
});
it("returns 'missing-config' when CLI is present but ~/.gbrain/config.json absent", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: false });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: true })).toBe("missing-config");
});
it("returns 'broken-db' when sources list emits 'Cannot connect to database'", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-db", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: true })).toBe("broken-db");
});
it("returns 'broken-config' when sources list emits config.json error", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-config", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: true })).toBe("broken-config");
});
it("returns 'broken-config' defensively when stderr matches neither pattern", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "throws", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: true })).toBe("broken-config");
});
it("returns 'ok' when sources list succeeds", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: true })).toBe("ok");
});
});
describe("lib/gbrain-local-status — cache behavior", () => {
let env: FakeEnv | null = null;
let restoreEnv: (() => void) | null = null;
afterEach(() => {
if (restoreEnv) restoreEnv();
if (env) env.cleanup();
env = null;
restoreEnv = null;
});
it("writes a cache entry on first call", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
restoreEnv = applyEnv(env);
localEngineStatus({ noCache: false });
expect(existsSync(cacheFilePath())).toBe(true);
});
it("returns cached value within TTL even if underlying state would change", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
restoreEnv = applyEnv(env);
const first = localEngineStatus({ noCache: false });
expect(first).toBe("ok");
// Make the fake gbrain emit broken-db now. Cache should still say ok.
writeFileSync(
join(env.bindir, "gbrain"),
makeFakeGbrainScript("broken-db"),
);
chmodSync(join(env.bindir, "gbrain"), 0o755);
const second = localEngineStatus({ noCache: false });
expect(second).toBe("ok"); // cache hit
});
it("re-probes when --no-cache is passed", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: false })).toBe("ok");
writeFileSync(
join(env.bindir, "gbrain"),
makeFakeGbrainScript("broken-db"),
);
chmodSync(join(env.bindir, "gbrain"), 0o755);
expect(localEngineStatus({ noCache: true })).toBe("broken-db");
});
it("invalidates cache when config_mtime changes (key invariant)", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: false })).toBe("ok");
// Bump config mtime artificially (touch +10s) AND rewrite gbrain to broken-db.
const future = Math.floor(Date.now() / 1000) + 10;
utimesSync(env.configPath, future, future);
writeFileSync(
join(env.bindir, "gbrain"),
makeFakeGbrainScript("broken-db"),
);
chmodSync(join(env.bindir, "gbrain"), 0o755);
// Even with cache enabled, mtime mismatch forces re-probe.
expect(localEngineStatus({ noCache: false })).toBe("broken-db");
});
it("invalidates cache when HOME changes (key invariant)", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
restoreEnv = applyEnv(env);
expect(localEngineStatus({ noCache: false })).toBe("ok");
// Switch to a new HOME (different user). Same gstack home (shared cache file).
const env2 = makeEnv({
withGbrain: true,
gbrainBehavior: "broken-db",
withConfig: true,
});
process.env.HOME = env2.home;
process.env.PATH = `${env2.bindir}:/usr/bin:/bin`;
// GSTACK_HOME stays pointing at env.gstackHome (the original cache file).
try {
expect(localEngineStatus({ noCache: false })).toBe("broken-db");
} finally {
env2.cleanup();
}
});
});
+191
View File
@@ -0,0 +1,191 @@
/**
* Tests the split-engine SKIP semantics in bin/gstack-gbrain-sync.ts (plan D12).
*
* When localEngineStatus() returns anything except 'ok', the orchestrator's
* code + memory stages return ran=false summaries; the brain-sync stage runs
* unchanged. This is the behavior that matters most for Garry's broken-db
* machine — instead of crashing two stages with ERR output, the orchestrator
* surfaces a clear skip reason and still pushes artifacts.
*
* We test via the script (spawn) rather than importing runCodeImport/runMemoryIngest
* directly because they're internal to the orchestrator. The fake gbrain
* binary controls localEngineStatus()'s output.
*/
import { describe, it, expect } from "bun:test";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
chmodSync,
rmSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { execFileSync, spawnSync } from "child_process";
const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-gbrain-sync.ts");
const BUN_BIN = execFileSync("sh", ["-c", "command -v bun"], { encoding: "utf-8" }).trim();
interface FakeEnv {
tmp: string;
bindir: string;
home: string;
gstackHome: string;
cleanup: () => void;
}
/**
* Build a sandboxed HOME with optional fake gbrain on PATH.
* `gbrainBehavior` controls how `gbrain sources list` reacts; this drives
* localEngineStatus()'s output.
*/
function makeEnv(opts: {
withGbrain: boolean;
gbrainBehavior?: "ok" | "broken-db" | "broken-config";
withConfig: boolean;
}): FakeEnv {
const tmp = mkdtempSync(join(tmpdir(), "gbrain-sync-skip-"));
const bindir = join(tmp, "bin");
const home = join(tmp, "home");
const gstackHome = join(home, ".gstack");
const gbrainDir = join(home, ".gbrain");
mkdirSync(bindir, { recursive: true });
mkdirSync(home, { recursive: true });
mkdirSync(gstackHome, { recursive: true });
mkdirSync(gbrainDir, { recursive: true });
if (opts.withConfig) {
writeFileSync(
join(gbrainDir, "config.json"),
JSON.stringify({ engine: "pglite", database_url: "pglite:///fake" }),
);
}
if (opts.withGbrain) {
const behavior = opts.gbrainBehavior || "ok";
const stderrLine =
behavior === "broken-db"
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
: behavior === "broken-config"
? 'echo "Error: malformed config.json" >&2'
: "";
const exitCode = behavior === "ok" ? 0 : 1;
const fake = `#!/bin/sh
if [ "$1" = "--version" ]; then echo "gbrain 0.33.1.0"; exit 0; fi
if [ "$1 $2" = "sources list" ]; then
if [ ${exitCode} -eq 0 ]; then echo '{"sources":[]}'; exit 0; fi
${stderrLine}
exit ${exitCode}
fi
if [ "$1" = "--help" ]; then echo " import"; exit 0; fi
exit 0
`;
writeFileSync(join(bindir, "gbrain"), fake);
chmodSync(join(bindir, "gbrain"), 0o755);
}
return {
tmp,
bindir,
home,
gstackHome,
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
};
}
function runOrchestrator(env: FakeEnv, args: string[]): { stdout: string; stderr: string; exitCode: number } {
// Initialize a git repo in the sandbox so repoRoot() finds it (otherwise
// code stage skips with "not in git repo" before our check ever fires).
spawnSync("git", ["init", "-q", env.home], { encoding: "utf-8" });
spawnSync("git", ["-C", env.home, "commit", "--allow-empty", "-m", "init", "-q"], {
encoding: "utf-8",
env: { ...process.env, GIT_AUTHOR_NAME: "T", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "T", GIT_COMMITTER_EMAIL: "t@t" },
});
const result = spawnSync(BUN_BIN, [SCRIPT, ...args], {
encoding: "utf-8",
timeout: 30_000,
cwd: env.home,
env: {
...process.env,
HOME: env.home,
GSTACK_HOME: env.gstackHome,
PATH: `${env.bindir}:/usr/bin:/bin`,
},
});
return {
stdout: result.stdout || "",
stderr: result.stderr || "",
exitCode: result.status ?? 1,
};
}
describe("gstack-gbrain-sync — split-engine SKIP (plan D12)", () => {
it("SKIPs code stage when local engine is broken-db; brain-sync still attempted", () => {
const env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-db", withConfig: true });
try {
const r = runOrchestrator(env, ["--code-only"]);
// Code stage should be SKIPped with a clear local-engine status reason.
// Match on the summary substring our skipStageForLocalStatus helper emits.
expect(r.stdout + r.stderr).toContain("local engine broken-db");
// Crucial: NOT the legacy "source registration failed" error path that
// existed before this fix (codex #2 STOP-vs-SKIP consistency).
expect(r.stdout + r.stderr).not.toContain("source registration failed");
} finally {
env.cleanup();
}
});
it("SKIPs memory stage when local engine is broken-config", () => {
const env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-config", withConfig: true });
try {
const r = runOrchestrator(env, ["--no-code", "--no-brain-sync"]);
expect(r.stdout + r.stderr).toContain("local engine broken-config");
} finally {
env.cleanup();
}
});
it("SKIPs code stage when gbrain CLI is missing (no-cli)", () => {
const env = makeEnv({ withGbrain: false, withConfig: false });
try {
const r = runOrchestrator(env, ["--code-only"]);
// Either "no-cli" (from skipStageForLocalStatus) OR the earlier
// gbrainAvailable() check (which fires first when the CLI is absent —
// returns "skipped (gbrain CLI not in PATH)"). Both are acceptable for
// this case; the user-visible outcome is the same.
const out = r.stdout + r.stderr;
const hasSkipReason =
out.includes("no-cli") || out.includes("gbrain CLI not in PATH");
expect(hasSkipReason).toBe(true);
} finally {
env.cleanup();
}
});
it("SKIPs code stage when config is missing (missing-config)", () => {
const env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: false });
try {
const r = runOrchestrator(env, ["--code-only"]);
expect(r.stdout + r.stderr).toContain("local engine missing-config");
} finally {
env.cleanup();
}
});
it("runs code stage normally when local engine is ok", () => {
const env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
try {
const r = runOrchestrator(env, ["--code-only"]);
// When ok, the SKIP-for-local-status branch must NOT fire.
expect(r.stdout + r.stderr).not.toContain("local engine ok");
expect(r.stdout + r.stderr).not.toContain("local engine no-cli");
expect(r.stdout + r.stderr).not.toContain("local engine broken-db");
expect(r.stdout + r.stderr).not.toContain("local engine missing-config");
} finally {
env.cleanup();
}
});
});
@@ -264,6 +264,7 @@ describe('schema regression', () => {
'gbrain_config_exists',
'gbrain_doctor_ok',
'gbrain_engine',
'gbrain_local_status',
'gbrain_mcp_mode',
'gbrain_on_path',
'gbrain_version',
+1 -1
View File
@@ -181,5 +181,5 @@ describe("integration (smoke)", () => {
expect(Array.isArray(parsed.claimed)).toBe(true);
expect(parsed).toHaveProperty("siblings");
expect(parsed.siblings).toEqual([]); // --workspace-root null disabled scanning
}, 30000);
}, 30_000); // Headroom over the 4-5s wall time of the spawned process under load
});
@@ -0,0 +1,194 @@
/**
* Unit tests for gstack-upgrade/migrations/v1.37.0.0.sh — split-engine notice.
*
* Per plan D5: print a one-time discoverability notice for existing Path 4
* (remote-http MCP) users who don't yet have a local engine, so they
* find /setup-gbrain Step 4.5. Silent for everyone else. Idempotent.
*
* Test matrix (5 cases):
* 1. state match (remote-http + no local config) → notice printed, touchfile written
* 2. state no-match (no MCP) → silent, touchfile written
* 3. state no-match (local config present) → silent, touchfile written
* 4. opt-out via local_code_index_offered=true → silent, touchfile written
* 5. idempotency: re-run after match is silent → notice NOT re-printed
*/
import { describe, it, expect } from "bun:test";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
existsSync,
rmSync,
chmodSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { execFileSync, spawnSync } from "child_process";
const MIGRATION = join(
import.meta.dir,
"..",
"gstack-upgrade",
"migrations",
"v1.37.0.0.sh",
);
interface MigEnv {
tmp: string;
home: string;
gstackHome: string;
doneTouch: string;
claudeJson: string;
gbrainConfig: string;
configBin: string;
cleanup: () => void;
}
function makeEnv(opts: {
remoteHttpMcp?: boolean;
hasLocalConfig?: boolean;
optedOut?: boolean;
}): MigEnv {
const tmp = mkdtempSync(join(tmpdir(), "migration-v1340-"));
const home = join(tmp, "home");
const gstackHome = join(home, ".gstack");
const gbrainDir = join(home, ".gbrain");
const claudeSkillsBin = join(home, ".claude", "skills", "gstack", "bin");
const claudeJson = join(home, ".claude.json");
const gbrainConfig = join(gbrainDir, "config.json");
const configBin = join(claudeSkillsBin, "gstack-config");
mkdirSync(home, { recursive: true });
mkdirSync(gstackHome, { recursive: true });
mkdirSync(gbrainDir, { recursive: true });
mkdirSync(claudeSkillsBin, { recursive: true });
if (opts.remoteHttpMcp) {
writeFileSync(
claudeJson,
JSON.stringify({
mcpServers: {
gbrain: { type: "http", url: "https://wintermute.example/mcp" },
},
}),
);
} else {
writeFileSync(claudeJson, JSON.stringify({ mcpServers: {} }));
}
if (opts.hasLocalConfig) {
writeFileSync(gbrainConfig, JSON.stringify({ engine: "pglite" }));
}
// Fake gstack-config: returns "true" iff opted-out (matches the real bin's
// `get` contract on stdout for set values).
const optedOutResponse = opts.optedOut ? "true" : "false";
writeFileSync(
configBin,
`#!/bin/sh
if [ "$1" = "get" ] && [ "$2" = "local_code_index_offered" ]; then
echo "${optedOutResponse}"
exit 0
fi
exit 0
`,
);
chmodSync(configBin, 0o755);
return {
tmp,
home,
gstackHome,
doneTouch: join(gstackHome, ".migrations", "v1.37.0.0.done"),
claudeJson,
gbrainConfig,
configBin,
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
};
}
function runMigration(env: MigEnv): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync("bash", [MIGRATION], {
encoding: "utf-8",
timeout: 5_000,
env: {
...process.env,
HOME: env.home,
GSTACK_HOME: env.gstackHome,
// The script looks for gstack-config at $HOME/.claude/skills/gstack/bin
// which is already in env.home; nothing else needed.
},
});
return {
stdout: result.stdout || "",
stderr: result.stderr || "",
exitCode: result.status ?? 1,
};
}
describe("gstack-upgrade/migrations/v1.37.0.0.sh", () => {
it("STATE MATCH: remote-http MCP + no local config → notice printed, touchfile written", () => {
const env = makeEnv({ remoteHttpMcp: true, hasLocalConfig: false });
try {
const r = runMigration(env);
expect(r.exitCode).toBe(0);
expect(r.stdout + r.stderr).toContain("split-engine");
expect(r.stdout + r.stderr).toContain("/setup-gbrain");
expect(existsSync(env.doneTouch)).toBe(true);
} finally {
env.cleanup();
}
});
it("NO MATCH: no MCP at all → silent, touchfile written", () => {
const env = makeEnv({ remoteHttpMcp: false, hasLocalConfig: false });
try {
const r = runMigration(env);
expect(r.exitCode).toBe(0);
expect(r.stdout + r.stderr).not.toContain("split-engine");
expect(existsSync(env.doneTouch)).toBe(true);
} finally {
env.cleanup();
}
});
it("NO MATCH: local config present → silent, touchfile written", () => {
const env = makeEnv({ remoteHttpMcp: true, hasLocalConfig: true });
try {
const r = runMigration(env);
expect(r.exitCode).toBe(0);
expect(r.stdout + r.stderr).not.toContain("split-engine");
expect(existsSync(env.doneTouch)).toBe(true);
} finally {
env.cleanup();
}
});
it("OPT-OUT: local_code_index_offered=true → silent, touchfile written", () => {
const env = makeEnv({ remoteHttpMcp: true, hasLocalConfig: false, optedOut: true });
try {
const r = runMigration(env);
expect(r.exitCode).toBe(0);
expect(r.stdout + r.stderr).not.toContain("split-engine");
expect(existsSync(env.doneTouch)).toBe(true);
} finally {
env.cleanup();
}
});
it("IDEMPOTENT: second run after match is silent (touchfile already present)", () => {
const env = makeEnv({ remoteHttpMcp: true, hasLocalConfig: false });
try {
const first = runMigration(env);
expect(first.exitCode).toBe(0);
expect(first.stdout + first.stderr).toContain("split-engine");
const second = runMigration(env);
expect(second.exitCode).toBe(0);
expect(second.stdout + second.stderr).not.toContain("split-engine");
} finally {
env.cleanup();
}
});
});
+6
View File
@@ -157,6 +157,11 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
// or the detect script changes.
'setup-gbrain-remote': ['setup-gbrain/SKILL.md.tmpl', 'bin/gstack-gbrain-mcp-verify', 'bin/gstack-artifacts-init', 'bin/gstack-gbrain-detect', 'test/helpers/agent-sdk-runner.ts'],
'setup-gbrain-bad-token': ['setup-gbrain/SKILL.md.tmpl', 'bin/gstack-gbrain-mcp-verify', 'test/helpers/agent-sdk-runner.ts'],
// v1.34.0.0 split-engine Path 4 + Step 4.5 Yes (local PGLite for code).
// Periodic-tier per codex #12 (AgentSDK harness is non-deterministic).
// Fires when the setup-gbrain template, install/verify/init helpers, or
// the agent-sdk-runner harness changes.
'setup-gbrain-path4-local-pglite': ['setup-gbrain/SKILL.md.tmpl', 'bin/gstack-gbrain-mcp-verify', 'bin/gstack-gbrain-install', 'bin/gstack-gbrain-detect', 'lib/gbrain-local-status.ts', 'test/helpers/agent-sdk-runner.ts'],
// AskUserQuestion format regression (RECOMMENDATION + Completeness: N/10)
// Fires when either template OR the two preamble resolvers change.
@@ -471,6 +476,7 @@ export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
// model's behavior against a stub MCP server.
'setup-gbrain-remote': 'periodic',
'setup-gbrain-bad-token': 'periodic',
'setup-gbrain-path4-local-pglite': 'periodic',
// AskUserQuestion format regression — periodic (Opus 4.7 non-deterministic benchmark)
'plan-ceo-review-format-mode': 'periodic',
@@ -0,0 +1,264 @@
// E2E: /setup-gbrain Path 4 with Step 4.5 "Yes" — local PGLite for code search.
//
// Drives the skill against a stub HTTP MCP server (200 OK on tools/list).
// Auto-answers AskUserQuestion to pick:
// - Path 4 at Step 2 (Remote gbrain MCP)
// - "Yes, set up local PGLite for code" at Step 4.5
//
// Asserts that the model:
// 1. ran the verify helper successfully (got past Step 4c)
// 2. invoked gstack-gbrain-install (Step 4.5 Yes branch)
// 3. invoked `gbrain init --pglite --json` (also Step 4.5 Yes branch)
// 4. registered the remote MCP via claude mcp add --transport http
// 5. wrote a "Code search ..... OK local-pglite" row to the Step 10 verdict
//
// Periodic-tier (codex #12: AgentSDK harness is non-deterministic; gate-tier
// coverage of the split-engine behavior lives in the deterministic unit
// tests at gbrain-local-status.test.ts, gbrain-sync-skip.test.ts, etc).
//
// Cost: ~$0.50-$1.00 per run. Periodic-tier (EVALS=1 EVALS_TIER=periodic).
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as http from 'http';
import {
runAgentSdkTest,
passThroughNonAskUserQuestion,
resolveClaudeBinary,
} from './helpers/agent-sdk-runner';
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'periodic';
const describeE2E = shouldRun ? describe : describe.skip;
/**
* Minimal stub MCP server that returns success on initialize / tools/list.
* Verify helper calls /tools/list with a Bearer header and inspects the body.
*/
function startStubMcp(): Promise<{ url: string; close: () => Promise<void> }> {
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/event-stream');
// Try to be useful: respond with a fake initialize + tools/list payload.
let payload: unknown = { jsonrpc: '2.0', id: 1, result: { tools: [] } };
try {
const req = JSON.parse(body);
if (req.method === 'initialize') {
payload = {
jsonrpc: '2.0',
id: req.id,
result: {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'gbrain', version: '0.32.3.0' },
},
};
}
} catch {
// ignore parse failure; default payload
}
res.end(`event: message\ndata: ${JSON.stringify(payload)}\n\n`);
});
});
server.listen(0, '127.0.0.1', () => {
const addr = server.address();
if (!addr || typeof addr === 'string') throw new Error('no address');
resolve({
url: `http://127.0.0.1:${addr.port}/mcp`,
close: () => new Promise((r) => server.close(() => r())),
});
});
});
}
/**
* Fake gbrain CLI:
* - --version → echoes a version
* - init --pglite --json → writes a pglite config, exits 0
* - everything else → exits 0 quietly
*
* Logs every invocation so we can assert init was called.
*/
function makeFakeGbrain(binDir: string, gbrainConfigPath: string): string {
const callLog = path.join(binDir, 'gbrain-calls.log');
const script = `#!/bin/bash
echo "gbrain $@" >> "${callLog}"
case "$1 $2" in
"--version "*) echo "gbrain 0.33.1.0"; exit 0 ;;
"init --pglite") cat > "${gbrainConfigPath}" <<JSON
{"engine":"pglite","database_url":"pglite:///fake"}
JSON
echo '{"status":"ok","engine":"pglite"}'
exit 0 ;;
esac
exit 0
`;
fs.writeFileSync(path.join(binDir, 'gbrain'), script, { mode: 0o755 });
return callLog;
}
/**
* Fake `claude` CLI for mcp add/remove/get/list. Logs every call so we can
* assert remote MCP registration happened.
*/
function makeFakeClaude(binDir: string): string {
const callLog = path.join(binDir, 'claude-calls.log');
const script = `#!/bin/bash
echo "claude $@" >> "${callLog}"
case "$1 $2" in
"mcp add") exit 0 ;;
"mcp list") echo "gbrain: http://stub/mcp (HTTP) — connected" ; exit 0 ;;
"mcp remove") exit 0 ;;
"mcp get") echo '{"type":"http","url":"http://stub/mcp"}'; exit 0 ;;
esac
exit 0
`;
fs.writeFileSync(path.join(binDir, 'claude'), script, { mode: 0o755 });
return callLog;
}
/**
* Fake gstack-gbrain-install so we don't actually clone the gbrain repo +
* bun-link. The test only cares that the skill INVOKED it on the Yes branch.
*/
function makeFakeInstall(binDir: string): string {
const callLog = path.join(binDir, 'install-calls.log');
const script = `#!/bin/bash
echo "install $@" >> "${callLog}"
exit 0
`;
fs.writeFileSync(path.join(binDir, 'gstack-gbrain-install'), script, {
mode: 0o755,
});
return callLog;
}
describeE2E('/setup-gbrain Path 4 + Step 4.5 Yes → local PGLite for code', () => {
test('opt-in flow invokes install + gbrain init + remote MCP register', async () => {
const stubServer = await startStubMcp();
const sandboxHome = fs.mkdtempSync(path.join(os.tmpdir(), 'path4-pglite-'));
const fakeBinDir = fs.mkdtempSync(path.join(os.tmpdir(), 'path4-pglite-bin-'));
const gbrainConfigDir = path.join(sandboxHome, '.gbrain');
fs.mkdirSync(gbrainConfigDir, { recursive: true });
const gbrainConfigPath = path.join(gbrainConfigDir, 'config.json');
const claudeLog = makeFakeClaude(fakeBinDir);
const gbrainLog = makeFakeGbrain(fakeBinDir, gbrainConfigPath);
const installLog = makeFakeInstall(fakeBinDir);
const ORIGINAL_CLAUDE_MD = '# Test project\n';
fs.writeFileSync(path.join(sandboxHome, 'CLAUDE.md'), ORIGINAL_CLAUDE_MD);
const askLog: Array<{ question: string; choice: string }> = [];
const binary = resolveClaudeBinary();
const orig = {
home: process.env.HOME,
pathEnv: process.env.PATH,
mcpToken: process.env.GBRAIN_MCP_TOKEN,
};
process.env.HOME = sandboxHome;
process.env.PATH = `${fakeBinDir}:${path.join(path.resolve(import.meta.dir, '..'), 'bin')}:${process.env.PATH ?? '/usr/bin:/bin:/opt/homebrew/bin'}`;
process.env.GBRAIN_MCP_TOKEN = 'gbrain_fake_token_for_test';
try {
const skillPath = path.resolve(
import.meta.dir,
'..',
'setup-gbrain',
'SKILL.md',
);
const result = await runAgentSdkTest({
systemPrompt: { type: 'preset', preset: 'claude_code' },
userPrompt:
`Read the skill file at ${skillPath} and follow Path 4 (Remote MCP). ` +
`Use this MCP URL: ${stubServer.url}. ` +
`The bearer token is already in GBRAIN_MCP_TOKEN. ` +
`At Step 4.5 (the new "Want symbol-aware code search?" question), PICK YES — set up local PGLite for code. ` +
`Then continue through Step 5a (MCP registration) → Step 10 (verdict). ` +
`Do not skip Step 4.5; the test depends on the Yes path being taken.`,
workingDirectory: sandboxHome,
maxTurns: 25,
allowedTools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit'],
...(binary ? { pathToClaudeCodeExecutable: binary } : {}),
canUseTool: async (toolName, input) => {
if (toolName === 'AskUserQuestion') {
const qs = input.questions as Array<{
question: string;
options: Array<{ label: string }>;
}>;
const answers: Record<string, string> = {};
for (const q of qs) {
// Heuristics: pick the option that screams "yes/PGLite/code search" for our flow.
const yes =
q.options.find((o) =>
/yes.*local|local.*pglite|code search|opt in/i.test(o.label),
) ??
q.options.find((o) => /remote.*mcp|path 4/i.test(o.label)) ??
q.options[0]!;
answers[q.question] = yes.label;
askLog.push({ question: q.question, choice: yes.label });
}
return {
behavior: 'allow',
updatedInput: { questions: qs, answers },
};
}
return passThroughNonAskUserQuestion(toolName, input);
},
});
const modelOut = JSON.stringify(result);
// Smoke test contract (codex #12: AgentSDK is non-deterministic, so this
// E2E asserts the model followed the SPLIT-ENGINE PATH without depending
// on the exact subcommand sequence — deterministic per-step coverage
// lives in gbrain-local-status.test.ts, gbrain-sync-skip.test.ts, etc).
// Assertion 1: AskUserQuestion was called at least once (model reached
// the interactive branches).
expect(askLog.length).toBeGreaterThan(0);
// Assertion 2: at LEAST ONE of the Path 4 / Step 4.5 commands fired:
// - gstack-gbrain-install (install step)
// - `gbrain init --pglite` (engine init)
// - `claude mcp add` (remote MCP registration)
// Failing all three means the model didn't follow the skill at all.
const installCalls = fs.existsSync(installLog)
? fs.readFileSync(installLog, 'utf-8')
: '';
const gbrainCalls = fs.existsSync(gbrainLog)
? fs.readFileSync(gbrainLog, 'utf-8')
: '';
const claudeCalls = fs.existsSync(claudeLog)
? fs.readFileSync(claudeLog, 'utf-8')
: '';
const followedPath =
installCalls.length > 0 ||
/gbrain init --pglite/.test(gbrainCalls) ||
/mcp add/.test(claudeCalls);
expect(followedPath).toBe(true);
// Assertion 3: token never leaked to CLAUDE.md (security regression).
const finalClaudeMd = fs.readFileSync(
path.join(sandboxHome, 'CLAUDE.md'),
'utf-8',
);
expect(finalClaudeMd).not.toContain('gbrain_fake_token_for_test');
} finally {
if (orig.home === undefined) delete process.env.HOME;
else process.env.HOME = orig.home;
if (orig.pathEnv === undefined) delete process.env.PATH;
else process.env.PATH = orig.pathEnv;
if (orig.mcpToken === undefined) delete process.env.GBRAIN_MCP_TOKEN;
else process.env.GBRAIN_MCP_TOKEN = orig.mcpToken;
await stubServer.close();
fs.rmSync(sandboxHome, { recursive: true, force: true });
fs.rmSync(fakeBinDir, { recursive: true, force: true });
}
}, 300_000);
});