/** * 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" | "slow"; 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"; // "slow": healthy engine, cold pooler connection (#1964) — sleeps past the // (test-lowered) probe timeout on `sources list`, then answers fine. const sourcesBlock = behavior === "slow" ? ` sleep 2 echo '{"sources":[]}' exit 0` : behavior === "ok" ? ` echo '{"sources":[]}' exit 0` : ` ${ behavior === "broken-db" ? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2' : 'echo "Error: malformed config.json" >&2' } exit 1`; const fake = `#!/bin/sh if [ "$1" = "--version" ]; then echo "gbrain 0.33.1.0"; exit 0; fi if [ "$1 $2" = "sources list" ]; then ${sourcesBlock} 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[], extraEnv: Record = {}, ): { 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`, ...extraEnv, }, }); return { stdout: result.stdout || "", stderr: result.stderr || "", exitCode: result.status ?? 1, }; } describe("gstack-gbrain-sync — split-engine SKIP (plan D12)", () => { it("PROCEEDS (with warning) when the engine probe times out — slow is not broken (#1964)", () => { const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true }); try { const r = runOrchestrator(env, ["--code-only"], { GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300", }); const out = r.stdout + r.stderr; // The stage must NOT be skipped with the local-engine reason... expect(out).not.toContain("local engine timeout"); expect(out).not.toContain("config.json is malformed"); // ...and the proceed-with-warning line must name the env knob. expect(out).toContain("GSTACK_GBRAIN_PROBE_TIMEOUT_MS"); } finally { env.cleanup(); } }, 30_000); // proceeding runs the real code-import path against the slow fake (~11s) it("memory stage also PROCEEDS (with warning) on probe timeout (#1964)", () => { const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true }); try { const r = runOrchestrator(env, ["--no-code", "--no-brain-sync"], { GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300", }); const out = r.stdout + r.stderr; expect(out).not.toContain("local engine timeout"); expect(out).toContain("memory: engine probe timed out"); } finally { env.cleanup(); } }, 30_000); it("dream stage also PROCEEDS (with warning) on probe timeout (#1964)", () => { const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true }); try { const r = runOrchestrator( env, ["--dream", "--no-code", "--no-memory", "--no-brain-sync"], { GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300" }, ); const out = r.stdout + r.stderr; expect(out).not.toContain("local engine timeout"); expect(out).toContain("dream: engine probe timed out"); } finally { env.cleanup(); } }, 30_000); 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(); } }); });