fix(brain-context-load): probe gbrain via execFile, not shell builtin (#1559)

gbrainAvailable() used `execFileSync("command", ["-v", "gbrain"])`,
which fails in any environment where the `command` builtin isn't on
the spawned process's PATH (most non-interactive shells). The probe
then reported gbrain as missing even when it was installed, and
context-load silently skipped vector/list queries.

Fix: probe `gbrain --version` directly with a 500ms timeout (matching
the rest of the file's MCP_TIMEOUT_MS). Same semantics, works
everywhere execFile works.

Contributed by @jbetala7 via #1560. Closes #1559.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-05-18 20:34:05 -07:00
parent 59a9b841af
commit 8b03c357ef
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 54 additions and 3 deletions

View File

@ -192,7 +192,10 @@ function resolveSkillFile(args: CliArgs): string | null {
function gbrainAvailable(): boolean {
try {
execFileSync("command", ["-v", "gbrain"], { stdio: "ignore" });
execFileSync("gbrain", ["--version"], {
stdio: "ignore",
timeout: MCP_TIMEOUT_MS,
});
return true;
} catch {
return false;

View File

@ -7,9 +7,9 @@
*/
import { describe, it, expect } from "bun:test";
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
import { chmodSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { delimiter, join } from "path";
import { spawnSync } from "child_process";
const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-brain-context-load.ts");
@ -27,6 +27,37 @@ function runScript(args: string[], env: Record<string, string> = {}): { stdout:
};
}
function writeFakeGbrain(binDir: string): void {
if (process.platform === "win32") {
writeFileSync(
join(binDir, "gbrain.cmd"),
"@echo off\r\nif \"%1\"==\"--version\" (\r\n echo gbrain 0.test\r\n) else (\r\n echo fake gbrain %*\r\n)\r\n",
"utf-8",
);
return;
}
const fakeBin = join(binDir, "gbrain");
writeFileSync(
fakeBin,
`#!/bin/sh
if [ "$1" = "--version" ]; then
echo "gbrain 0.test"
else
echo "fake gbrain $*"
fi
`,
"utf-8",
);
chmodSync(fakeBin, 0o755);
}
function prependPath(binDir: string): Record<string, string> {
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") || "PATH";
const currentPath = process.env[pathKey] || "";
return { [pathKey]: `${binDir}${delimiter}${currentPath}` };
}
describe("gstack-brain-context-load CLI", () => {
it("--help exits 0 with usage", () => {
const r = runScript(["--help"]);
@ -204,6 +235,23 @@ gbrain:
});
describe("gstack-brain-context-load — graceful gbrain absence", () => {
it("uses gbrain when a binary is available on PATH", () => {
const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-"));
const binDir = join(dir, "bin");
mkdirSync(binDir);
writeFakeGbrain(binDir);
try {
const r = runScript(["--repo", "test-repo", "--explain"], prependPath(binDir));
expect(r.exitCode).toBe(0);
expect(r.stderr).toContain("OK");
expect(r.stderr).not.toContain("gbrain CLI missing");
expect(r.stdout).toContain("fake gbrain list_pages");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("vector + list queries still complete (with SKIP) when gbrain CLI is missing", () => {
// We can't easily un-install gbrain; rely on the helper's own missing-binary
// detection. The default manifest uses kind: list which calls gbrain. If