mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
A CLI installed by nvm, Homebrew or a user-level npm prefix lives on a PATH that only a login shell sets up. Codeman running under systemd or launchd does not get that PATH — launchd hands a job `/usr/bin:/bin:/usr/sbin:/sbin` — so every resolver reported the CLI as unavailable on installs where it is plainly there and works from a terminal. Each of the six resolvers had its own hand-rolled copy of the same PATH walk, so the fix is factored into one shared `createCliExecutableResolver()` with an explicit lookup order: the server process PATH, then common install directories in order, then an interactive login shell as the last resort. Only the last step spawns anything, and only when the cheap lookups have already missed. Also adds `formatCliNotFoundMessage()`, so a failure explains where it looked instead of just asserting the CLI is missing. Its diagnostics are bounded and control characters are flattened, so a not-found message cannot dump arbitrary environment data. Success is cached and failure is retried, so installing a CLI while the server is running is picked up without a restart. Net -103 lines across the six resolvers. Behaviour is unchanged wherever the CLI was already on the process PATH: that remains the first thing checked. Tests: 20 cases in test/cli-executable-resolver.test.ts covering the precedence order, login-shell-only resolution, the caching rule, unsafe-name rejection, and the bounded diagnostics.
112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
/**
|
|
* @fileoverview Tests for the Antigravity CLI resolver wrapper.
|
|
*/
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createAntigravityResolverForTest, isAntigravityAvailable } from '../src/utils/antigravity-cli-resolver.js';
|
|
import type { CliResolution, CliResolverHost } from '../src/utils/cli-executable-resolver.js';
|
|
|
|
const availabilityResolution = vi.hoisted(() => ({ current: null as CliResolution | null }));
|
|
|
|
vi.mock('../src/utils/cli-executable-resolver.js', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../src/utils/cli-executable-resolver.js')>();
|
|
return {
|
|
...actual,
|
|
createCliExecutableResolver: (options: { binary: string; searchDirs: string[] }, host?: CliResolverHost) =>
|
|
host
|
|
? actual.createCliExecutableResolver(options, host)
|
|
: {
|
|
resolve: () => availabilityResolution.current,
|
|
diagnostics: () => ({
|
|
binary: options.binary,
|
|
processPath: '/service/bin',
|
|
shellPath: '/bin/zsh',
|
|
shellArgs: ['-l'],
|
|
searchDirs: [...options.searchDirs],
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
function createHost(
|
|
options: {
|
|
processPathResult?: string | null;
|
|
loginShellResults?: Array<string | null>;
|
|
existingPaths?: string[];
|
|
} = {}
|
|
): CliResolverHost {
|
|
const loginShellResults = [...(options.loginShellResults ?? [])];
|
|
const existingPaths = new Set(options.existingPaths ?? []);
|
|
return {
|
|
processPath: '/service/bin',
|
|
shellPath: '/bin/zsh',
|
|
shellArgs: ['-l'],
|
|
findOnProcessPath: () => options.processPathResult ?? null,
|
|
findInLoginShell: () => loginShellResults.shift() ?? null,
|
|
exists: (path) => existingPaths.has(path),
|
|
};
|
|
}
|
|
|
|
describe('Antigravity CLI resolver', () => {
|
|
beforeEach(() => {
|
|
availabilityResolution.current = null;
|
|
});
|
|
|
|
it('resolves agy from the service PATH', () => {
|
|
const binaryPath = '/service/bin/agy';
|
|
const resolver = createAntigravityResolverForTest(
|
|
createHost({ processPathResult: binaryPath, existingPaths: [binaryPath] })
|
|
);
|
|
|
|
expect(resolver.resolve()?.directory).toBe('/service/bin');
|
|
});
|
|
|
|
it('falls back to a common install directory', () => {
|
|
const binaryPath = join(homedir(), '.local', 'bin', 'agy');
|
|
const resolver = createAntigravityResolverForTest(createHost({ existingPaths: [binaryPath] }));
|
|
|
|
expect(resolver.resolve()?.directory).toBe(join(homedir(), '.local', 'bin'));
|
|
});
|
|
|
|
it('resolves agy found only by the login shell', () => {
|
|
const binaryPath = '/login-shell/bin/agy';
|
|
const resolver = createAntigravityResolverForTest(
|
|
createHost({ loginShellResults: [binaryPath], existingPaths: [binaryPath] })
|
|
);
|
|
|
|
expect(resolver.resolve()?.directory).toBe('/login-shell/bin');
|
|
});
|
|
|
|
it('returns null when agy is unavailable', () => {
|
|
const resolver = createAntigravityResolverForTest(createHost());
|
|
|
|
expect(resolver.resolve()).toBeNull();
|
|
});
|
|
|
|
it('retries a failed lookup and caches the first successful login-shell discovery', () => {
|
|
const binaryPath = '/late-login-shell/bin/agy';
|
|
const resolver = createAntigravityResolverForTest(
|
|
createHost({ loginShellResults: [null, binaryPath], existingPaths: [binaryPath] })
|
|
);
|
|
|
|
expect(resolver.resolve()).toBeNull();
|
|
expect(resolver.resolve()?.binaryPath).toBe(binaryPath);
|
|
expect(resolver.resolve()?.binaryPath).toBe(binaryPath);
|
|
});
|
|
|
|
it('reports the public wrapper as available when agy resolves', () => {
|
|
availabilityResolution.current = {
|
|
binaryPath: '/service/bin/agy',
|
|
directory: '/service/bin',
|
|
source: 'process-path',
|
|
};
|
|
|
|
expect(isAntigravityAvailable()).toBe(true);
|
|
});
|
|
|
|
it('reports the public wrapper as unavailable when agy does not resolve', () => {
|
|
expect(isAntigravityAvailable()).toBe(false);
|
|
});
|
|
});
|