mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-07 14:06:42 +02:00
df9f7b69c9
Replaces 75 LOC of fork-side reimplementation (PATH parsing, Windows PATHEXT, case-insensitive Path/PATH, X_OK) with a thin wrapper around Bun.which() — the runtime built-in that already does all of it. New file is ~70 LOC including the override + arg-prefix logic the runtime doesn't cover. Override branch fixed: GSTACK_CLAUDE_BIN=wsl now resolves through Bun.which() just like a bare claude lookup would. The McGluut fork's claude-bin.ts only handled absolute-path overrides; bare commands silently returned null. Passing the override value through Bun.which fixes the documented use case for free. Five hardcoded claude spawn sites rewired through resolveClaudeCommand: - browse/src/security-classifier.ts:396 — version probe - browse/src/security-classifier.ts:496 — Haiku transcript classifier - scripts/preflight-agent-sdk.ts — preflight binary pinning - test/helpers/providers/claude.ts — LLM judge availability + run - test/helpers/agent-sdk-runner.ts — SDK harness binary resolver All retain their existing degrade-on-missing semantics. Tests: browse/test/claude-bin.test.ts has 9 unit tests including the override-PATH-resolution case the fork's version got wrong. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import { resolveClaudeCommand, resolveClaudeBinary } from '../src/claude-bin';
|
|
|
|
// Empty env baseline — no PATH, no overrides — ensures no environmental claude binary leaks in.
|
|
const EMPTY_ENV = { PATH: '', Path: '' } as NodeJS.ProcessEnv;
|
|
|
|
describe('claude-bin', () => {
|
|
test('no override, no PATH match → returns null', () => {
|
|
expect(resolveClaudeCommand(EMPTY_ENV)).toBeNull();
|
|
expect(resolveClaudeBinary(EMPTY_ENV)).toBeNull();
|
|
});
|
|
|
|
test('absolute-path override returned as-is', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
GSTACK_CLAUDE_BIN: '/opt/custom/claude',
|
|
});
|
|
expect(got).toEqual({ command: '/opt/custom/claude', argsPrefix: [] });
|
|
});
|
|
|
|
test('CLAUDE_BIN works as fallback alias for GSTACK_CLAUDE_BIN', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
CLAUDE_BIN: '/opt/custom/claude',
|
|
});
|
|
expect(got?.command).toBe('/opt/custom/claude');
|
|
});
|
|
|
|
test('GSTACK_CLAUDE_BIN takes precedence over CLAUDE_BIN', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
GSTACK_CLAUDE_BIN: '/explicit/path',
|
|
CLAUDE_BIN: '/fallback/path',
|
|
});
|
|
expect(got?.command).toBe('/explicit/path');
|
|
});
|
|
|
|
test('PATH-resolvable override goes through Bun.which (the bug the fork shipped)', () => {
|
|
// Make a fake binary in a temp dir, point PATH at it, set override to bare command name.
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-bin-test-'));
|
|
const fakeBin = path.join(tmpDir, 'fake-claude-cli');
|
|
fs.writeFileSync(fakeBin, '#!/bin/sh\necho fake\n');
|
|
fs.chmodSync(fakeBin, 0o755);
|
|
try {
|
|
const got = resolveClaudeCommand({
|
|
PATH: tmpDir,
|
|
GSTACK_CLAUDE_BIN: 'fake-claude-cli',
|
|
});
|
|
expect(got?.command).toBe(fakeBin);
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('override pointing at missing binary → null (no silent fallback to bare claude)', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
GSTACK_CLAUDE_BIN: 'definitely-not-a-real-binary-xyz',
|
|
});
|
|
expect(got).toBeNull();
|
|
});
|
|
|
|
test('GSTACK_CLAUDE_BIN_ARGS as JSON array → parsed argsPrefix', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
GSTACK_CLAUDE_BIN: '/opt/custom/claude',
|
|
GSTACK_CLAUDE_BIN_ARGS: '["--no-cache", "--verbose"]',
|
|
});
|
|
expect(got?.argsPrefix).toEqual(['--no-cache', '--verbose']);
|
|
});
|
|
|
|
test('GSTACK_CLAUDE_BIN_ARGS as scalar string → treated as single argument', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
GSTACK_CLAUDE_BIN: '/opt/custom/claude',
|
|
GSTACK_CLAUDE_BIN_ARGS: 'claude',
|
|
});
|
|
expect(got?.argsPrefix).toEqual(['claude']);
|
|
});
|
|
|
|
test('argsPrefix empty when no override args set', () => {
|
|
const got = resolveClaudeCommand({
|
|
...EMPTY_ENV,
|
|
GSTACK_CLAUDE_BIN: '/opt/custom/claude',
|
|
});
|
|
expect(got?.argsPrefix).toEqual([]);
|
|
});
|
|
});
|