import { describe, it, expect } from 'vitest'; import { resolveLaunchSpec, resolveAcpSpawnArgs } from '../acp-spawn.js'; import { buildResolvedRegistry } from '../provider-config-registry.js'; import type { CoderProvidersFile } from '../provider-config.js'; import { PROVIDERS } from '../provider-registry.js'; /** Resolved def for a provider id under the given config (default: no override). */ function builtin(name: string, providers: CoderProvidersFile['providers'] = {}) { const def = buildResolvedRegistry(PROVIDERS, { providers }).get(name); if (!def) throw new Error(`no resolved def for ${name}`); return def; } describe('resolveLaunchSpec', () => { // --- byte-identical built-in regression (the HARD CONSTRAINT) --------------- // These argv values are the pre-v2.3 resolveAcpSpawnArgs switch outputs and // MUST NOT change. spawn() is `spawn(spec.binary, spec.args, ...)`, so argv // parity here is dispatch parity. it('opencode (no override) → byte-identical argv ["acp"], binary = installPath', () => { const spec = resolveLaunchSpec(builtin('opencode'), '/usr/bin/opencode'); expect(spec).not.toBeNull(); expect(spec!.args).toEqual(['acp']); // pre-v2.3 value expect(spec!.binary).toBe('/usr/bin/opencode'); expect(spec!.env).toBeUndefined(); // cross-check against the switch source-of-truth expect(spec!.args).toEqual(resolveAcpSpawnArgs('opencode')); }); it('goose → ["acp"], qwen → ["--acp"] (byte-identical)', () => { expect(resolveLaunchSpec(builtin('goose'), '/usr/bin/goose')!.args).toEqual(['acp']); expect(resolveLaunchSpec(builtin('qwen'), '/usr/bin/qwen')!.args).toEqual(['--acp']); }); it('built-in with null installPath falls back to the bare id (pre-v2.3 `installPath ?? agent`)', () => { const spec = resolveLaunchSpec(builtin('opencode'), null); expect(spec!.binary).toBe('opencode'); expect(spec!.args).toEqual(['acp']); }); it('non-ACP / unknown provider → null (claude has no ACP argv)', () => { expect(resolveLaunchSpec(builtin('claude'), '/usr/bin/claude')).toBeNull(); expect(resolveLaunchSpec(builtin('boocode'), null)).toBeNull(); }); // --- config-driven launch (the new capability) ------------------------------ it('custom ACP entry → configured command + env reach the spec', () => { const def = builtin('amp-acp', { 'amp-acp': { extends: 'acp', label: 'Amp', command: ['amp-acp', '--acp'], env: { AMP_KEY: 'x' } }, }); const spec = resolveLaunchSpec(def, '/usr/local/bin/amp-acp'); expect(spec).not.toBeNull(); expect(spec!.binary).toBe('amp-acp'); // command[0], not the resolved install path expect(spec!.args).toEqual(['--acp']); // command.slice(1) expect(spec!.env).toEqual({ AMP_KEY: 'x' }); }); it('built-in WITH a config command override uses the override, not the switch default', () => { const def = builtin('opencode', { opencode: { command: ['opencode', 'acp', '--verbose'], env: { DEBUG: '1' } } }); const spec = resolveLaunchSpec(def, '/usr/bin/opencode'); expect(spec!.binary).toBe('opencode'); expect(spec!.args).toEqual(['acp', '--verbose']); expect(spec!.env).toEqual({ DEBUG: '1' }); }); }); describe('acp-dispatch spawn wiring (documented pass-through)', () => { // dispatchViaAcp spawns `spawn(spec.binary, spec.args, { env: { ...process.env, ...spec.env } })`. // The env merge layers config env over process.env; for a built-in with no // config env, spec.env is undefined → { ...process.env } (byte-identical). it('built-in with no config env yields an undefined spec.env (→ plain process.env at spawn)', () => { expect(resolveLaunchSpec(builtin('opencode'), '/usr/bin/opencode')!.env).toBeUndefined(); }); });