mirror of https://github.com/garrytan/gstack.git
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:
parent
59a9b841af
commit
8b03c357ef
|
|
@ -192,7 +192,10 @@ function resolveSkillFile(args: CliArgs): string | null {
|
||||||
|
|
||||||
function gbrainAvailable(): boolean {
|
function gbrainAvailable(): boolean {
|
||||||
try {
|
try {
|
||||||
execFileSync("command", ["-v", "gbrain"], { stdio: "ignore" });
|
execFileSync("gbrain", ["--version"], {
|
||||||
|
stdio: "ignore",
|
||||||
|
timeout: MCP_TIMEOUT_MS,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from "bun:test";
|
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 { tmpdir } from "os";
|
||||||
import { join } from "path";
|
import { delimiter, join } from "path";
|
||||||
import { spawnSync } from "child_process";
|
import { spawnSync } from "child_process";
|
||||||
|
|
||||||
const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-brain-context-load.ts");
|
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", () => {
|
describe("gstack-brain-context-load CLI", () => {
|
||||||
it("--help exits 0 with usage", () => {
|
it("--help exits 0 with usage", () => {
|
||||||
const r = runScript(["--help"]);
|
const r = runScript(["--help"]);
|
||||||
|
|
@ -204,6 +235,23 @@ gbrain:
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("gstack-brain-context-load — graceful gbrain absence", () => {
|
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", () => {
|
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
|
// 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
|
// detection. The default manifest uses kind: list which calls gbrain. If
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue