diff --git a/bin/gstack-config b/bin/gstack-config index d679f682b..be3b29512 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -693,10 +693,12 @@ case "${1:-}" in # Summarize for the user. Use python (already required elsewhere) to # parse the JSON portably; fall back to grep if python is unavailable. + # The file goes in on stdin (E6, #1967): bash opens the path, so a native + # Windows Python under Git Bash never has to resolve an MSYS /c/... path. 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) + STATUS=$("$PYTHON_CMD" -c "import json,sys; d=json.load(sys.stdin); print(d.get('gbrain_local_status','unknown'))" < "$DETECTION_FILE" 2>/dev/null || echo unknown) + VERSION=$("$PYTHON_CMD" -c "import json,sys; d=json.load(sys.stdin); print(d.get('gbrain_version') or 'unknown')" < "$DETECTION_FILE" 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/') diff --git a/test/gstack-config-gbrain-refresh.test.ts b/test/gstack-config-gbrain-refresh.test.ts new file mode 100644 index 000000000..84b5e8332 --- /dev/null +++ b/test/gstack-config-gbrain-refresh.test.ts @@ -0,0 +1,50 @@ +/** + * E6 (#1967): under Git Bash on Windows, a native Windows Python first on PATH + * cannot open an MSYS-style /c/... path, so gbrain-refresh always summarized + * the detection file as "local-status: unknown". The file is now read by bash + * and handed to Python on stdin. The stub Python below refuses any path + * argument, as a native Windows Python does with an MSYS path, and parses stdin. + */ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..'); +let tmp: string; +let binDir: string; +let home: string; + +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 }); + for (const f of ['gstack-config', 'gstack-state-root.sh']) { + fs.copyFileSync(path.join(ROOT, 'bin', f), path.join(binDir, f)); + fs.chmodSync(path.join(binDir, f), 0o755); + } + fs.writeFileSync(path.join(binDir, 'gstack-gbrain-detect'), + `#!/usr/bin/env bash\nprintf '%s\\n' '{"gbrain_on_path":true,"gbrain_local_status":"no-config","gbrain_version":"0.35.8.0"}'\n`, { mode: 0o755 }); + // A Python that cannot open() the state path but reads stdin, like native + // Windows Python handed an MSYS path. Bun stands in for its JSON parse. + fs.writeFileSync(path.join(binDir, 'python3'), `#!/usr/bin/env bash +case "$2" in *"open("*) echo "FileNotFoundError: [Errno 2] No such file or directory" >&2; exit 1 ;; esac +exec ${JSON.stringify(process.execPath)} -e 'const d = JSON.parse(require("fs").readFileSync(0, "utf8")); console.log(process.argv[1].includes("gbrain_version") ? (d.gbrain_version || "unknown") : (d.gbrain_local_status || "unknown"))' "$2" +`, { mode: 0o755 }); +}); +afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); + +describe('gstack-config gbrain-refresh (E6)', () => { + test('reads the detection file through stdin, so the status is not masked as unknown', () => { + const r = spawnSync('bash', [path.join(binDir, 'gstack-config'), 'gbrain-refresh'], { + encoding: 'utf8', timeout: 30_000, + env: { PATH: `${binDir}:${process.env.PATH ?? '/usr/bin:/bin'}`, HOME: home, GSTACK_HOME: home, GSTACK_STATE_ROOT: home }, + }); + expect(r.status, r.stderr).toBe(0); + expect(r.stdout).toContain('local-status: no-config'); + expect(r.stdout).not.toContain('local-status: unknown'); + }); +});