fix(gbrain): classify probe timeout as its own status; sync proceeds instead of skipping (#1964)

The 5s engine probe misclassified healthy-but-slow engines (cold Supabase
pooler connections measured at 6.9-10.7s) as broken-config, so /sync-gbrain
silently skipped code+memory and told the user their config was malformed.

- New "timeout" status: probe killed at the deadline with no recognized
  stderr pattern. Default deadline is now 15s, overridable via
  GSTACK_GBRAIN_PROBE_TIMEOUT_MS (tests set 300ms against a fake that
  sleeps 2s).
- Sync stages PROCEED on timeout with a stderr warning naming the env knob;
  a genuinely-dead engine surfaces its real error at the first operation
  instead of a false config diagnosis.
- Consistency everywhere "ok" gated behavior: gstack-gbrain-detect --is-ok
  exits 0 on timeout, and gen-skill-docs' detection gate accepts it, so a
  slow engine no longer silently suppresses brain-aware features.
- Status cache: key now includes the effective probe timeout (raising it
  invalidates a cached timeout) and GBRAIN_HOME; config detection honors
  GBRAIN_HOME so relocated-home users stop being misclassified as
  missing-config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-11 20:27:46 -07:00
co-authored by Claude Fable 5
parent 8e4662e930
commit da06ef1504
10 changed files with 278 additions and 54 deletions
+41 -12
View File
@@ -42,7 +42,7 @@ interface FakeEnv {
*/
function makeEnv(opts: {
withGbrain: boolean;
gbrainBehavior?: "ok" | "broken-db" | "broken-config";
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "slow";
withConfig: boolean;
}): FakeEnv {
const tmp = mkdtempSync(join(tmpdir(), "gbrain-sync-skip-"));
@@ -65,19 +65,26 @@ function makeEnv(opts: {
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;
// "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
if [ ${exitCode} -eq 0 ]; then echo '{"sources":[]}'; exit 0; fi
${stderrLine}
exit ${exitCode}
${sourcesBlock}
fi
if [ "$1" = "--help" ]; then echo " import"; exit 0; fi
exit 0
@@ -95,7 +102,11 @@ exit 0
};
}
function runOrchestrator(env: FakeEnv, args: string[]): { stdout: string; stderr: string; exitCode: number } {
function runOrchestrator(
env: FakeEnv,
args: string[],
extraEnv: Record<string, 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" });
@@ -113,6 +124,7 @@ function runOrchestrator(env: FakeEnv, args: string[]): { stdout: string; stderr
HOME: env.home,
GSTACK_HOME: env.gstackHome,
PATH: `${env.bindir}:/usr/bin:/bin`,
...extraEnv,
},
});
return {
@@ -123,6 +135,23 @@ function runOrchestrator(env: FakeEnv, args: string[]): { stdout: string; stderr
}
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("SKIPs code stage when local engine is broken-db; brain-sync still attempted", () => {
const env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-db", withConfig: true });
try {