diff --git a/bin/gstack-config b/bin/gstack-config index 01defcef86..d43e554d18 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -381,18 +381,20 @@ case "${1:-}" in fi mv "$DETECTION_FILE.tmp" "$DETECTION_FILE" - # Summarize for the user. Use python (already required elsewhere) to - # parse the JSON portably; fall back to grep if python is unavailable. - PYTHON_CMD=$(command -v python3 || command -v python || true) - if [ -n "$PYTHON_CMD" ]; then - STATUS=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_local_status','unknown'))" 2>/dev/null || echo unknown) - VERSION=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_version') or 'unknown')" 2>/dev/null || echo unknown) - else - STATUS=$(grep -o '"gbrain_local_status":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/1円/') - VERSION=$(grep -o '"gbrain_version":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/1円/') - [ -z "$STATUS" ] && STATUS=unknown - [ -z "$VERSION" ] && VERSION=unknown + # Summarize for the user. Prefer the shell parser because Git Bash can put + # native Windows Python first on PATH, and that Python cannot open MSYS-style + # /c/... paths. Keep Python only as a fallback if the flat-field grep misses. + STATUS=$(grep -o '"gbrain_local_status":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/1円/' | head -1 || true) + VERSION=$(grep -o '"gbrain_version":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/1円/' | head -1 || true) + if [ -z "$STATUS" ] || [ -z "$VERSION" ]; then + PYTHON_CMD=$(command -v python3 || command -v python || true) + if [ -n "$PYTHON_CMD" ]; then + [ -z "$STATUS" ] && STATUS=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_local_status','unknown'))" 2>/dev/null || echo unknown) + [ -z "$VERSION" ] && VERSION=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_version') or 'unknown')" 2>/dev/null || echo unknown) + fi fi + [ -z "$STATUS" ] && STATUS=unknown + [ -z "$VERSION" ] && VERSION=unknown case "$STATUS" in ok) diff --git a/test/gstack-config-gbrain-refresh.test.ts b/test/gstack-config-gbrain-refresh.test.ts new file mode 100644 index 0000000000..9ca764d09b --- /dev/null +++ b/test/gstack-config-gbrain-refresh.test.ts @@ -0,0 +1,69 @@ +/** + * Regression coverage for #1967: Git Bash on Windows can put a native Windows + * Python first on PATH. That Python cannot open MSYS-style /c/... paths, so + * gstack-config must not let a Python JSON parse failure mask a valid detection + * file as local-status: unknown. + */ +import { describe, test, expect, beforeEach, afterEach } from "bun:test"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { spawnSync } from "child_process"; + +const ROOT = path.resolve(import.meta.dir, ".."); +const SOURCE_CONFIG = path.join(ROOT, "bin", "gstack-config"); + +let tmp: string; +let binDir: string; +let home: string; + +function writeExecutable(file: string, content: string) { + fs.writeFileSync(file, content, { mode: 0o755 }); +} + +function runGbrainRefresh(): { code: number; out: string; err: string } { + const r = spawnSync(path.join(binDir, "gstack-config"), ["gbrain-refresh"], { + encoding: "utf8", + env: { + ...process.env, + HOME: home, + GSTACK_HOME: home, + GSTACK_STATE_ROOT: home, + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, + }, + }); + return { code: r.status ?? 0, out: r.stdout ?? "", err: r.stderr ?? "" }; +} + +beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gbrain-refresh-")); + binDir = path.join(tmp, "bin"); + home = path.join(tmp, "home"); + fs.mkdirSync(binDir, { recursive: true }); + fs.mkdirSync(home, { recursive: true }); + fs.copyFileSync(SOURCE_CONFIG, path.join(binDir, "gstack-config")); + fs.chmodSync(path.join(binDir, "gstack-config"), 0o755); +}); + +afterEach(() => { + fs.rmSync(tmp, { recursive: true, force: true }); +}); + +describe("gstack-config gbrain-refresh", () => { + test("does not report unknown when PATH python cannot open the detection file", () => { + writeExecutable( + path.join(binDir, "gstack-gbrain-detect"), + `#!/usr/bin/env bash\nprintf '%s\\n' '{"gbrain_on_path":true,"gbrain_local_status":"ok","gbrain_version":"0.35.8.0"}'\n`, + ); + writeExecutable( + path.join(binDir, "python3"), + `#!/usr/bin/env bash\necho "FileNotFoundError: native Windows Python cannot open this MSYS path">&2\nexit 1\n`, + ); + + const r = runGbrainRefresh(); + + expect(r.code).toBe(0); + expect(r.out).toContain("Detected gbrain v0.35.8.0"); + expect(r.out).not.toContain("local-status: unknown"); + }); +});