Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions lib/gstack-memory-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, appendFileSync } from "fs";
import { dirname, join } from "path";
import { execSync, execFileSync } from "child_process";
import { execSync, execFileSync, spawnSync } from "child_process";
import { homedir } from "os";

// ── Types ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -247,9 +247,16 @@ export function detectEngineTier(): EngineDetect {
function freshDetectEngineTier(): EngineDetect {
const now = Date.now();
try {
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
const parsed = JSON.parse(out);
const engine: EngineTier = parsed?.engine === "supabase" ? "supabase" : parsed?.engine === "pglite" ? "pglite" : "unknown";
const result = spawnSync("gbrain", ["doctor", "--json", "--fast"], {
encoding: "utf-8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"], env: process.env,
});
if (result.error) return { engine: "unknown", detected_at: now, schema_version: 1 };

const parsed = JSON.parse(result.stdout);
const engine = detectEngineFromDoctorJson(parsed);
if (engine === "unknown" && process.env.GSTACK_DEBUG) {
process.stderr.write(`[gstack-memory-helpers] unable to detect gbrain engine from doctor JSON: ${JSON.stringify(parsed)}\n`);
}
return {
engine,
supabase_url: parsed?.supabase_url || undefined,
Expand All @@ -261,6 +268,23 @@ function freshDetectEngineTier(): EngineDetect {
}
}

function detectEngineFromDoctorJson(parsed: unknown): EngineTier {
const obj = (parsed && typeof parsed === "object" ? parsed : {}) as Record<string, unknown>;
const direct = [obj.engine, obj.engine_kind, obj.backend, obj.engine_type].find(isEngineTier);
if (direct) return direct;
const check = Array.isArray(obj.checks)
? obj.checks.find((c) => c && typeof c === "object" && /engine|backend|kind/i.test(String((c as Record<string, unknown>).name)))
: null;
const nested = check
? [check.value, check.engine, check.kind].find(isEngineTier)
: null;
return nested || "unknown";
}

function isEngineTier(value: unknown): value is EngineTier {
return value === "supabase" || value === "pglite";
}

// ── Public: parseSkillManifest ────────────────────────────────────────────

/**
Expand Down
33 changes: 31 additions & 2 deletions test/gstack-memory-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Free-tier (~50ms total). Runs in `bun test`.
*/

import { describe, it, expect, beforeEach, afterAll } from "bun:test";
import { describe, it, expect, beforeEach, afterEach, afterAll } from "bun:test";
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
Expand Down Expand Up @@ -272,19 +272,34 @@ describe("withErrorContext", () => {

describe("detectEngineTier", () => {
let savedHome: string | undefined;
let savedPath: string | undefined;
let testHome: string;

beforeEach(() => {
savedHome = process.env.GSTACK_HOME;
savedPath = process.env.PATH;
testHome = mkdtempSync(join(tmpdir(), "gstack-test-engine-"));
process.env.GSTACK_HOME = testHome;
process.env.PATH = `${testHome}:${savedPath || ""}`;
writeFileSync(join(testHome, "gbrain"), "#!/bin/sh\nprintf '%s\\n' \"$GSTACK_TEST_GBRAIN_STDOUT\"\nexit \"$GSTACK_TEST_GBRAIN_STATUS\"\n", { mode: 0o755 });
mockDoctor('{"engine":"pglite","status":"ok"}');
});

afterAll(() => {
afterEach(() => {
if (savedPath === undefined) delete process.env.PATH;
else process.env.PATH = savedPath;
if (savedHome === undefined) delete process.env.GSTACK_HOME;
else process.env.GSTACK_HOME = savedHome;
delete process.env.GSTACK_TEST_GBRAIN_STDOUT;
delete process.env.GSTACK_TEST_GBRAIN_STATUS;
rmSync(testHome, { recursive: true, force: true });
});

function mockDoctor(stdout: string, exitCode = 0) {
process.env.GSTACK_TEST_GBRAIN_STDOUT = stdout;
process.env.GSTACK_TEST_GBRAIN_STATUS = String(exitCode);
}

it("returns a valid EngineDetect shape (engine, detected_at, schema_version)", () => {
const result = detectEngineTier();
expect(["pglite", "supabase", "unknown"]).toContain(result.engine);
Expand All @@ -293,6 +308,20 @@ describe("detectEngineTier", () => {
expect(result.detected_at).toBeGreaterThan(0);
});

for (const [name, stdout, exitCode, supabaseUrl] of [
["preserves schema_version 1 engine detection from doctor output", '{"engine":"supabase","supabase_url":"https://example.supabase.co","status":"ok"}', 0, "https://example.supabase.co"],
["detects schema_version 2 engine from top-level doctor fields", '{"status":"ok","schema_version":2,"health_score":100,"engine_kind":"supabase","checks":[]}', 0, undefined],
["detects schema_version 2 engine from checks even when doctor exits non-zero", '{"status":"warn","schema_version":2,"health_score":80,"checks":[{"name":"resolver_health","status":"warn"},{"name":"backend","value":"supabase"}]}', 1, undefined],
] as const) {
it(name, () => {
mockDoctor(stdout, exitCode);
const result = detectEngineTier();
expect(result.engine).toBe("supabase");
if (supabaseUrl) expect(result.supabase_url).toBe(supabaseUrl);
expect(result.schema_version).toBe(1);
});
}

it("writes a cache file at ~/.gstack/.gbrain-engine-cache.json", () => {
detectEngineTier();
const cachePath = join(testHome, ".gbrain-engine-cache.json");
Expand Down
Loading