mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-23 18:20:00 +02:00
fix(gbrain): engine detection survives gbrain ≥0.25 schema + non-zero doctor exit
freshDetectEngineTier() in lib/gstack-memory-helpers.ts returned engine:
"unknown" for every Supabase user on gbrain ≥0.25. Two stacking bugs:
1. execSync("gbrain doctor --json --fast 2>/dev/null") threw on non-zero
exit. gbrain doctor exits 1 whenever health_score < 100, which is
essentially every fresh install due to resolver_health warnings. The
JSON output never reached the parser.
2. gbrain ≥0.25 shipped schema_version:2 doctor output that dropped the
top-level 'engine' field entirely.
Result: every /sync-gbrain on Supabase logged 'engine=unknown' and skipped
all sync stages silently.
Fix:
- Replace execSync with execFileSync (no shell, no bash-specific 2>/dev/null
redirect; portable to Windows).
- Recover stdout from the thrown error object so non-zero exits still parse.
- Fall back to reading gbrain's config.json (respecting GBRAIN_HOME env var,
defaulting to ~/.gbrain/config.json) when doctor output doesn't surface
an engine field.
- Add logGbrainError() helper that appends one-line JSONL to
~/.gstack/.gbrain-errors.jsonl on parse failure, so future regressions
leave a forensic trail.
The "supabase" tier here means "remote postgres" in practice — gbrain
config uses engine:"postgres" for both real Supabase and any other
remote postgres (e.g. local-postgres-for-testing). Downstream sync code
treats them identically, so the label compression is intentional and
documented inline.
Regression test: existing detectEngineTier suite now isolates HOME +
GBRAIN_HOME + PATH to temp dirs (closes a flake source where the prior
tests would read whatever was on the reviewer's machine). New test
forces gbrain off PATH, writes a synthetic config.json with
engine:"postgres", asserts detectEngineTier() returns
engine:"supabase".
Fixes #1415. Patch shape contributed by Shiv @shivasymbl (tested on
gstack v1.31.0.0 + gbrain v0.31.3 + Supabase).
This commit is contained in:
@@ -272,17 +272,36 @@ describe("withErrorContext", () => {
|
||||
|
||||
describe("detectEngineTier", () => {
|
||||
let savedHome: string | undefined;
|
||||
let savedGbrainHome: string | undefined;
|
||||
let savedRealHome: string | undefined;
|
||||
let savedPath: string | undefined;
|
||||
let testHome: string;
|
||||
let testGbrainHome: string;
|
||||
|
||||
beforeEach(() => {
|
||||
savedHome = process.env.GSTACK_HOME;
|
||||
savedGbrainHome = process.env.GBRAIN_HOME;
|
||||
savedRealHome = process.env.HOME;
|
||||
savedPath = process.env.PATH;
|
||||
testHome = mkdtempSync(join(tmpdir(), "gstack-test-engine-"));
|
||||
testGbrainHome = mkdtempSync(join(tmpdir(), "gstack-test-gbrain-"));
|
||||
process.env.GSTACK_HOME = testHome;
|
||||
process.env.GBRAIN_HOME = testGbrainHome;
|
||||
// Isolate HOME too — even though gbrainConfigPath() prefers GBRAIN_HOME
|
||||
// when set, defense-in-depth against future code reading ~/.gbrain
|
||||
// directly. See #1415 codex review finding #6.
|
||||
process.env.HOME = testHome;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (savedHome === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = savedHome;
|
||||
if (savedGbrainHome === undefined) delete process.env.GBRAIN_HOME;
|
||||
else process.env.GBRAIN_HOME = savedGbrainHome;
|
||||
if (savedRealHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedRealHome;
|
||||
if (savedPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = savedPath;
|
||||
});
|
||||
|
||||
it("returns a valid EngineDetect shape (engine, detected_at, schema_version)", () => {
|
||||
@@ -307,4 +326,19 @@ describe("detectEngineTier", () => {
|
||||
const second = detectEngineTier();
|
||||
expect(second.detected_at).toBe(first.detected_at);
|
||||
});
|
||||
|
||||
it("falls back to GBRAIN_HOME/config.json when gbrain doctor omits engine (schema_version:2 case)", () => {
|
||||
// Regression test for #1415: gbrain >=0.25 doctor output dropped the
|
||||
// top-level `engine` field. The detect path must fall back to config.json.
|
||||
// We force the doctor call to fail (PATH stripped of gbrain) and write a
|
||||
// synthetic config to GBRAIN_HOME so the fallback path is deterministic.
|
||||
process.env.PATH = "/nonexistent-no-gbrain-here";
|
||||
writeFileSync(
|
||||
join(testGbrainHome, "config.json"),
|
||||
JSON.stringify({ engine: "postgres", database_url: "postgresql://test/example" }),
|
||||
"utf-8"
|
||||
);
|
||||
const result = detectEngineTier();
|
||||
expect(result.engine).toBe("supabase");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user