mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
The brain-sync stage failed on EVERY Windows run with "is not recognized as an internal or external command", so /sync-gbrain always reported ERR brain-sync among otherwise green stages. #1731 gave these spawns shell: NEEDS_SHELL_ON_WINDOWS. That is correct for the gbrain.cmd shim and does nothing here: shell:true routes through cmd.exe, which resolves .cmd/.bat via PATHEXT but has no concept of a shebang, so an extension-less bash script is rejected outright. A .cmd shim needs a shell; a shebang script needs an interpreter. The two cases look identical and are not. The failure was quiet rather than loud. artifacts_sync_mode defaults to pushing curated artifacts to git, so a Windows user's learnings piled up uncommitted in ~/.gstack indefinitely while the sync report showed one red line out of four. New bashScriptInvocation() resolves Git for Windows' bash explicitly and passes the script as argv[0]. It prefers Git bash over a bare `bash` on PATH because WindowsApps ships a bash.exe that is the WSL launcher, which would read C:\... as a Linux path; GSTACK_BASH overrides for unusual installs; forward slashes because bash treats backslashes as escapes; and it returns null when no bash exists so the stage says so plainly instead of surfacing an unactionable spawn error. The #1731 tripwire asserted the shape that does not work, so it now asserts the opposite (never a raw spawnSync(brainSyncPath, ...)) and six unit tests cover the resolver. Verified on Windows: the stage now reports "OK brain-sync curated artifacts pushed (4.2s)" and the artifacts repo committed + pushed on its own. Affected-test set unchanged at 14 pre-existing failures before and after, with 6 new passing tests.
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
import { bashScriptInvocation } from "../lib/gbrain-exec";
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "..");
|
|
const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), "utf-8");
|
|
|
|
// #1731 tripwire. Windows can't spawn the `gbrain` shim (gbrain.cmd) or the bash
|
|
// shebang script gstack-brain-sync without a shell; the fix gates `shell: true`
|
|
// behind NEEDS_SHELL_ON_WINDOWS. These static checks fail CI if a refactor adds
|
|
// a gbrain/brain-sync child spawn without the Windows shell flag, since macOS/
|
|
// Linux CI can't exercise the Windows path at runtime.
|
|
describe("#1731 gbrain spawns carry the Windows shell flag", () => {
|
|
test("NEEDS_SHELL_ON_WINDOWS is platform-gated in gbrain-exec.ts", () => {
|
|
const src = read("lib/gbrain-exec.ts");
|
|
expect(src).toMatch(/export const NEEDS_SHELL_ON_WINDOWS\s*=\s*process\.platform === "win32"/);
|
|
});
|
|
|
|
// Every direct `gbrain` child spawn in these files must be matched by a
|
|
// shell:NEEDS_SHELL_ON_WINDOWS flag. Count openers vs flags as a cheap,
|
|
// refactor-resistant invariant.
|
|
const gbrainSpawnFiles = [
|
|
"lib/gbrain-exec.ts",
|
|
"lib/gbrain-sources.ts",
|
|
"lib/gbrain-local-status.ts",
|
|
];
|
|
for (const rel of gbrainSpawnFiles) {
|
|
test(`${rel}: every gbrain spawn has shell:NEEDS_SHELL_ON_WINDOWS`, () => {
|
|
const src = read(rel);
|
|
const spawnOpeners = src.match(/(spawnSync|spawn|execFileSync)\("gbrain"/g)?.length ?? 0;
|
|
const shellFlags = src.match(/shell:\s*NEEDS_SHELL_ON_WINDOWS/g)?.length ?? 0;
|
|
expect(spawnOpeners).toBeGreaterThan(0);
|
|
expect(shellFlags).toBeGreaterThanOrEqual(spawnOpeners);
|
|
});
|
|
}
|
|
|
|
// NOT the brain-sync script. `shell: true` is right for the gbrain.cmd shim
|
|
// and wrong for a bash shebang script: cmd.exe resolves .cmd/.bat via PATHEXT
|
|
// and has no concept of a shebang, so gstack-brain-sync came back as "is not
|
|
// recognized as an internal or external command" on EVERY Windows run. It
|
|
// needs an interpreter, not a shell — see bashScriptInvocation.
|
|
test("orchestrator invokes brain-sync through bash, never a raw spawn", () => {
|
|
const src = read("bin/gstack-gbrain-sync.ts");
|
|
expect(src).toMatch(/bashScriptInvocation\(brainSyncPath, \["--discover-new"\]\)/);
|
|
expect(src).toMatch(/bashScriptInvocation\(brainSyncPath, \["--once"\]\)/);
|
|
// The old shape must not come back: it fails silently-ish on Windows.
|
|
expect(src).not.toMatch(/spawnSync\(brainSyncPath,/);
|
|
expect(src).not.toMatch(/spawnSync\(brainSyncPath,[\s\S]*?shell:\s*NEEDS_SHELL_ON_WINDOWS/);
|
|
});
|
|
});
|
|
|
|
describe("bashScriptInvocation", () => {
|
|
const WIN_BASH = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
|
|
test("POSIX execs the script directly, no interpreter needed", () => {
|
|
const inv = bashScriptInvocation("/home/u/.claude/skills/gstack/bin/gstack-brain-sync", ["--once"], {
|
|
platform: "linux",
|
|
});
|
|
expect(inv).toEqual({
|
|
cmd: "/home/u/.claude/skills/gstack/bin/gstack-brain-sync",
|
|
argv: ["--once"],
|
|
shell: false,
|
|
});
|
|
});
|
|
|
|
test("Windows routes through Git bash with the script as argv[0]", () => {
|
|
const inv = bashScriptInvocation("C:\\Users\\u\\.claude\\skills\\gstack\\bin\\gstack-brain-sync", ["--once"], {
|
|
platform: "win32",
|
|
exists: (p) => p === WIN_BASH,
|
|
env: {},
|
|
});
|
|
expect(inv?.cmd).toBe(WIN_BASH);
|
|
expect(inv?.argv[1]).toBe("--once");
|
|
});
|
|
|
|
test("Windows forward-slashes the script path", () => {
|
|
// bash treats backslashes as escapes, so a verbatim Windows path loses its
|
|
// separators and the script is never found.
|
|
const inv = bashScriptInvocation("C:\\Users\\u\\bin\\gstack-brain-sync", [], {
|
|
platform: "win32",
|
|
exists: (p) => p === WIN_BASH,
|
|
env: {},
|
|
});
|
|
expect(inv?.argv[0]).toBe("C:/Users/u/bin/gstack-brain-sync");
|
|
expect(inv?.argv[0]).not.toContain("\\");
|
|
});
|
|
|
|
test("never asks for a shell — cmd.exe is what broke this", () => {
|
|
const inv = bashScriptInvocation("C:\\x\\gstack-brain-sync", [], {
|
|
platform: "win32",
|
|
exists: (p) => p === WIN_BASH,
|
|
env: {},
|
|
});
|
|
expect(inv?.shell).toBe(false);
|
|
});
|
|
|
|
test("GSTACK_BASH overrides the search for unusual installs", () => {
|
|
const custom = "D:\\tools\\git\\bin\\bash.exe";
|
|
const inv = bashScriptInvocation("C:\\x\\gstack-brain-sync", [], {
|
|
platform: "win32",
|
|
exists: (p) => p === custom || p === WIN_BASH,
|
|
env: { GSTACK_BASH: custom },
|
|
});
|
|
expect(inv?.cmd).toBe(custom);
|
|
});
|
|
|
|
test("returns null when Windows has no bash, so the caller can say why", () => {
|
|
const inv = bashScriptInvocation("C:\\x\\gstack-brain-sync", [], {
|
|
platform: "win32",
|
|
exists: () => false,
|
|
env: {},
|
|
});
|
|
expect(inv).toBeNull();
|
|
});
|
|
});
|