import { describe, it, expect, vi } from 'vitest'; import { mkdtempSync, mkdirSync, symlinkSync, rmSync, lstatSync, writeFileSync } from 'node:fs'; import { tmpdir, homedir } from 'node:os'; import { join } from 'node:path'; import { CLAUDEX_CONFIG_DIR_ENV, CLAUDEX_DEFAULT_PRIMARY_MODEL, CLAUDEX_DEFAULT_SMALL_FAST_MODEL, CLAUDEX_CREDENTIAL_ENV_RE, defaultClaudexConfigDir, assertIsolatedConfigDir, resolveClaudexConfigDir, resolveClaudexModels, buildClaudexEnv, buildClaudexBanner, buildClaudexContractNote, runClaudexProxyGate, launchClaudex, type ClaudexHarnessAdapter, } from './claudex.js'; import { CLAUDEX_PROXY_URL, type PreflightReport } from './claudex-proxy.js'; // ─── helpers ───────────────────────────────────────────────────────────────── function makeReport(overrides: Partial = {}): PreflightReport { return { binaryPresent: true, binaryPath: '/usr/bin/claude-code-proxy', auth: { state: 'valid' }, live: true, listenerVerdict: 'ok', needsReauth: false, ok: true, problems: [], ...overrides, }; } function okAdapter(overrides: Partial = {}): ClaudexHarnessAdapter { return { harnessPreflight: () => {}, composePrompt: () => '# Composed Claude contract', exec: () => {}, ...overrides, }; } // Identity canonicalizer + no-op FS deps so config-dir logic is tested purely. const idCanon = (p: string): string => p; const noFsDeps = { canonicalize: idCanon, mkdir: () => {}, isSymlink: () => false }; // ─── isolated config dir (HARD SECURITY REQ 1 — provable isolation) ─────────── describe('defaultClaudexConfigDir', () => { it('is namespaced under the mosaic home, never ~/.claude', () => { const dir = defaultClaudexConfigDir('/home/agent/.config/mosaic'); expect(dir).toBe(join('/home/agent/.config/mosaic', 'claudex', 'home')); expect(dir).not.toBe(join(homedir(), '.claude')); }); }); describe('assertIsolatedConfigDir — the isolation guard is provable', () => { const realClaude = '/home/agent/.claude'; it('accepts a dir that does not resolve to ~/.claude', () => { const safe = '/home/agent/.config/mosaic/claudex/home'; expect( assertIsolatedConfigDir(safe, { realClaudeDir: realClaude, canonicalize: idCanon }), ).toBe(safe); }); it('REJECTS a candidate that is literally ~/.claude', () => { expect(() => assertIsolatedConfigDir(realClaude, { realClaudeDir: realClaude, canonicalize: idCanon }), ).toThrow(/refusing/i); }); it('REJECTS a descendant of ~/.claude (would pollute the real tree)', () => { expect(() => assertIsolatedConfigDir('/home/agent/.claude/projects/x', { realClaudeDir: realClaude, canonicalize: idCanon, }), ).toThrow(/refusing/i); }); it('REJECTS a candidate that canonically resolves to ~/.claude (symlink, both sides canonicalized)', () => { const canon = (p: string): string => (p === '/home/agent/link' ? realClaude : p); expect(() => assertIsolatedConfigDir('/home/agent/link', { realClaudeDir: realClaude, canonicalize: canon, }), ).toThrow(/refusing/i); }); it('canonicalizes the ~/.claude side too (real dir itself may be a symlink)', () => { // realClaudeDir is a symlink whose canonical target equals the candidate's target. const canon = (p: string): string => p === '/home/agent/.claude' || p === '/home/agent/link' ? '/canonical/claude' : p; expect(() => assertIsolatedConfigDir('/home/agent/link', { realClaudeDir: realClaude, canonicalize: canon, }), ).toThrow(/refusing/i); }); it('REJECTS an empty or whitespace candidate (fail closed)', () => { expect(() => assertIsolatedConfigDir('', { realClaudeDir: realClaude, canonicalize: idCanon }), ).toThrow(); expect(() => assertIsolatedConfigDir(' ', { realClaudeDir: realClaude, canonicalize: idCanon }), ).toThrow(); }); it('REJECTS a relative candidate (must be absolute)', () => { expect(() => assertIsolatedConfigDir('relative/dir', { realClaudeDir: realClaude, canonicalize: idCanon }), ).toThrow(/absolute/i); }); }); describe('resolveClaudexConfigDir', () => { it('uses the namespaced default and never the ambient CLAUDE_CONFIG_DIR', () => { // Ambient CLAUDE_CONFIG_DIR is deliberately ignored (it could be ~/.claude). const env = { CLAUDE_CONFIG_DIR: join(homedir(), '.claude') }; const dir = resolveClaudexConfigDir(env, { mosaicHome: '/home/agent/.config/mosaic', realClaudeDir: '/home/agent/.claude', ...noFsDeps, }); expect(dir).toBe(join('/home/agent/.config/mosaic', 'claudex', 'home')); }); it('honors the dedicated override env when it is safe', () => { const env = { [CLAUDEX_CONFIG_DIR_ENV]: '/home/agent/custom-claudex' }; const dir = resolveClaudexConfigDir(env, { mosaicHome: '/home/agent/.config/mosaic', realClaudeDir: '/home/agent/.claude', ...noFsDeps, }); expect(dir).toBe('/home/agent/custom-claudex'); }); it('REJECTS a dedicated override that points at ~/.claude (before creating anything)', () => { const mkdir = vi.fn(); const env = { [CLAUDEX_CONFIG_DIR_ENV]: '/home/agent/.claude' }; expect(() => resolveClaudexConfigDir(env, { mosaicHome: '/home/agent/.config/mosaic', realClaudeDir: '/home/agent/.claude', canonicalize: idCanon, mkdir, isSymlink: () => false, }), ).toThrow(/refusing/i); expect(mkdir).not.toHaveBeenCalled(); }); it('TOCTOU: REJECTS when the created target is itself a symlink (pre-created race)', () => { const env = {}; expect(() => resolveClaudexConfigDir(env, { mosaicHome: '/home/agent/.config/mosaic', realClaudeDir: '/home/agent/.claude', canonicalize: idCanon, mkdir: () => {}, isSymlink: () => true, // the just-ensured dir is a symlink → fail closed }), ).toThrow(/refusing|symlink/i); }); it('real-FS: creates the isolated dir 0700 and returns its canonical path', () => { const root = mkdtempSync(join(tmpdir(), 'claudex-cfg-')); try { const mosaicHome = join(root, '.config', 'mosaic'); const dir = resolveClaudexConfigDir({}, { mosaicHome, realClaudeDir: join(root, '.claude') }); expect(dir).toBe(join(mosaicHome, 'claudex', 'home')); const st = lstatSync(dir); expect(st.isDirectory()).toBe(true); // 0700 (owner-only) — mask off the type bits. expect(st.mode & 0o777).toBe(0o700); } finally { rmSync(root, { recursive: true, force: true }); } }); it('real-FS: catches an override whose ancestor symlinks into ~/.claude', () => { const root = mkdtempSync(join(tmpdir(), 'claudex-cfg-')); try { const realClaudeDir = join(root, 'dot-claude'); mkdirSync(realClaudeDir, { recursive: true }); const link = join(root, 'link'); // link -> dot-claude symlinkSync(realClaudeDir, link, 'dir'); const override = join(link, 'sub'); // resolves under ~/.claude expect(() => resolveClaudexConfigDir( { [CLAUDEX_CONFIG_DIR_ENV]: override }, { mosaicHome: join(root, '.config', 'mosaic'), realClaudeDir }, ), ).toThrow(/refusing/i); } finally { rmSync(root, { recursive: true, force: true }); } }); it('FAIL CLOSED: default canonicalizer rethrows a non-ENOENT error (ELOOP) instead of a literal fallback', () => { // A symlink loop makes realpathSync throw ELOOP. The guard must NOT swallow // it as "does not exist yet, keep walking up" and return a literal path — // it must fail closed. (REQ 1: fails CLOSED on any uncertainty.) const root = mkdtempSync(join(tmpdir(), 'claudex-loop-')); try { const a = join(root, 'a'); const b = join(root, 'b'); symlinkSync(b, a, 'dir'); // a -> b symlinkSync(a, b, 'dir'); // b -> a (loop) const looped = join(a, 'home'); // canonicalizing this hits ELOOP // No canonicalize dep → the real defaultCanonicalizeIntended runs. expect(() => assertIsolatedConfigDir(looped, { realClaudeDir: join(root, '.claude') }), ).toThrow(); } finally { rmSync(root, { recursive: true, force: true }); } }); it('FAIL CLOSED: default isSymlink rethrows a non-ENOENT error (ENOTDIR) rather than reporting "not a symlink"', () => { // A candidate whose parent is a regular FILE makes lstat throw ENOTDIR. // The post-create symlink check must fail closed, not treat it as safe. const root = mkdtempSync(join(tmpdir(), 'claudex-notdir-')); try { const file = join(root, 'afile'); writeFileSync(file, 'x'); const candidate = join(file, 'child'); // parent is a file → ENOTDIR on lstat expect(() => // Bypass the guard/mkdir side-effects; only the default isSymlink runs live. resolveClaudexConfigDir( { [CLAUDEX_CONFIG_DIR_ENV]: candidate }, { realClaudeDir: join(root, '.claude'), canonicalize: idCanon, mkdir: () => {}, // isSymlink omitted → real defaultIsSymlink runs on the ENOTDIR path. }, ), ).toThrow(); } finally { rmSync(root, { recursive: true, force: true }); } }); it('FAIL CLOSED: an injected canonicalize throwing EACCES is not swallowed', () => { const eacces = Object.assign(new Error('permission denied'), { code: 'EACCES' }); expect(() => resolveClaudexConfigDir( { [CLAUDEX_CONFIG_DIR_ENV]: '/home/agent/custom-claudex' }, { realClaudeDir: '/home/agent/.claude', canonicalize: () => { throw eacces; }, mkdir: () => {}, isSymlink: () => false, }, ), ).toThrow(/permission denied/); }); }); // ─── model-tier map (P3) ────────────────────────────────────────────────────── describe('resolveClaudexModels', () => { it('defaults primary=sol / smallFast=luna', () => { expect(resolveClaudexModels({})).toEqual({ primary: CLAUDEX_DEFAULT_PRIMARY_MODEL, smallFast: CLAUDEX_DEFAULT_SMALL_FAST_MODEL, }); expect(CLAUDEX_DEFAULT_PRIMARY_MODEL).toBe('gpt-5.6-sol'); expect(CLAUDEX_DEFAULT_SMALL_FAST_MODEL).toBe('gpt-5.6-luna'); }); it('env-provided values WIN over defaults', () => { expect( resolveClaudexModels({ ANTHROPIC_MODEL: 'gpt-x', ANTHROPIC_SMALL_FAST_MODEL: 'gpt-y' }), ).toEqual({ primary: 'gpt-x', smallFast: 'gpt-y' }); }); it('ignores blank env values (falls back to defaults)', () => { expect( resolveClaudexModels({ ANTHROPIC_MODEL: ' ', ANTHROPIC_SMALL_FAST_MODEL: '' }), ).toEqual({ primary: CLAUDEX_DEFAULT_PRIMARY_MODEL, smallFast: CLAUDEX_DEFAULT_SMALL_FAST_MODEL, }); }); }); // ─── env injection (HARD SECURITY REQ 2 — zero token leakage) ───────────────── describe('buildClaudexEnv — zero token leakage', () => { const models = { primary: 'gpt-5.6-sol', smallFast: 'gpt-5.6-luna' }; const configDir = '/home/agent/.config/mosaic/claudex/home'; it('sets only ANTHROPIC_AUTH_TOKEN=unused and points at the loopback proxy', () => { const env = buildClaudexEnv({}, { configDir, models }); expect(env.ANTHROPIC_AUTH_TOKEN).toBe('unused'); expect(env.ANTHROPIC_BASE_URL).toBe(CLAUDEX_PROXY_URL); expect(env.CLAUDE_CONFIG_DIR).toBe(configDir); expect(env.ANTHROPIC_MODEL).toBe('gpt-5.6-sol'); expect(env.ANTHROPIC_SMALL_FAST_MODEL).toBe('gpt-5.6-luna'); }); it('OVERWRITES an inherited real auth token with the literal "unused"', () => { const env = buildClaudexEnv( { ANTHROPIC_AUTH_TOKEN: 'sk-ant-realsecret-should-never-flow' }, { configDir, models }, ); expect(env.ANTHROPIC_AUTH_TOKEN).toBe('unused'); }); it('DELETES ANTHROPIC_API_KEY so no real Anthropic key reaches the local proxy', () => { const env = buildClaudexEnv( { ANTHROPIC_API_KEY: 'sk-ant-api03-realkey' }, { configDir, models }, ); expect('ANTHROPIC_API_KEY' in env).toBe(false); expect(env.ANTHROPIC_API_KEY).toBeUndefined(); }); it('sweeps the WHOLE credential-bearing env family (token/api-key/secret/oauth), not just two', () => { const env = buildClaudexEnv( { ANTHROPIC_API_KEY: 'sk-ant-api03-leak', CLAUDE_CODE_OAUTH_TOKEN: 'oauth-leak', SOME_SERVICE_TOKEN: 'tok-leak', VENDOR_API_KEY: 'key-leak', DB_SECRET: 'secret-leak', HARMLESS: 'kept', PATH: '/usr/bin', }, { configDir, models }, ); expect(env.ANTHROPIC_API_KEY).toBeUndefined(); expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); expect(env.SOME_SERVICE_TOKEN).toBeUndefined(); expect(env.VENDOR_API_KEY).toBeUndefined(); expect(env.DB_SECRET).toBeUndefined(); // Non-credential vars the harness needs are preserved. expect(env.HARMLESS).toBe('kept'); expect(env.PATH).toBe('/usr/bin'); }); it('neutralizes Bedrock/Vertex provider switches so Claude cannot bypass the proxy (REQ 2)', () => { // CLAUDE_CODE_USE_BEDROCK / _USE_VERTEX are ROUTING switches: their mere // presence makes Claude Code route to AWS Bedrock / GCP Vertex against the // ambient cloud credential chain — reaching the real Anthropic API and // bypassing ANTHROPIC_BASE_URL (the loopback proxy) entirely. They MUST be // gone from the composed env regardless of the launching env. const env = buildClaudexEnv( { CLAUDE_CODE_USE_BEDROCK: '1', CLAUDE_CODE_USE_VERTEX: '1', CLAUDE_CODE_SKIP_BEDROCK_AUTH: '1', CLAUDE_CODE_SKIP_VERTEX_AUTH: '1', AWS_ACCESS_KEY_ID: 'AKIAREAL', AWS_SECRET_ACCESS_KEY: 'realsecret', AWS_SESSION_TOKEN: 'realsession', AWS_BEARER_TOKEN_BEDROCK: 'bearer-bedrock-real', AWS_REGION: 'us-east-1', GOOGLE_APPLICATION_CREDENTIALS: '/home/agent/gcp.json', GOOGLE_CLOUD_ACCESS_TOKEN: 'gcp-token-real', PATH: '/usr/bin', }, { configDir, models }, ); // Routing switches gone by construction. expect('CLAUDE_CODE_USE_BEDROCK' in env).toBe(false); expect('CLAUDE_CODE_USE_VERTEX' in env).toBe(false); expect('CLAUDE_CODE_SKIP_BEDROCK_AUTH' in env).toBe(false); expect('CLAUDE_CODE_SKIP_VERTEX_AUTH' in env).toBe(false); // Cloud credentials swept — none of the Claude-capable creds survive. expect(env.AWS_ACCESS_KEY_ID).toBeUndefined(); expect(env.AWS_SECRET_ACCESS_KEY).toBeUndefined(); expect(env.AWS_SESSION_TOKEN).toBeUndefined(); expect(env.AWS_BEARER_TOKEN_BEDROCK).toBeUndefined(); expect(env.AWS_REGION).toBeUndefined(); expect(env.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined(); expect(env.GOOGLE_CLOUD_ACCESS_TOKEN).toBeUndefined(); // The proxy routing is still the only path. expect(env.ANTHROPIC_BASE_URL).toBe(CLAUDEX_PROXY_URL); expect(env.ANTHROPIC_AUTH_TOKEN).toBe('unused'); expect(env.PATH).toBe('/usr/bin'); }); it('closes the mid-string _KEY / _SECRET gap (STRIPE_SECRET_KEY, SSH_PRIVATE_KEY)', () => { const env = buildClaudexEnv( { STRIPE_SECRET_KEY: 'sk-live-real', SSH_PRIVATE_KEY: '-----BEGIN OPENSSH PRIVATE KEY-----', HARMLESS: 'kept', }, { configDir, models }, ); expect(env.STRIPE_SECRET_KEY).toBeUndefined(); expect(env.SSH_PRIVATE_KEY).toBeUndefined(); expect(env.HARMLESS).toBe('kept'); }); it('no credential-NAMED key in the composed env carries a real-looking value', () => { const env = buildClaudexEnv( { ANTHROPIC_API_KEY: 'sk-ant-api03-leak', ANTHROPIC_AUTH_TOKEN: 'access_token_leak', SOME_JWT_TOKEN: 'eyJhbGciOiJIUzI1NiJ9.payload.sig', REFRESH_SECRET: 'refresh_token_value', }, { configDir, models }, ); for (const [name, value] of Object.entries(env)) { if (CLAUDEX_CREDENTIAL_ENV_RE.test(name)) { // Any surviving credential-named var must carry only a safe sentinel value. expect(value).not.toMatch(/sk-(ant|proj)-/); expect(value).not.toMatch(/access_token|refresh_token/); expect(value).not.toMatch(/eyJ[A-Za-z0-9_-]+\./); // JWT } } }); it('honors a caller-provided baseUrl override (loopback default otherwise)', () => { const env = buildClaudexEnv({}, { configDir, models, baseUrl: 'http://127.0.0.1:9999' }); expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:9999'); }); it('returns a fresh object without mutating the base env', () => { const base = { EXISTING: 'kept' }; const env = buildClaudexEnv(base, { configDir, models }); expect(env.EXISTING).toBe('kept'); expect(base).not.toHaveProperty('ANTHROPIC_AUTH_TOKEN'); }); }); // ─── EXPERIMENTAL classification (P4) ───────────────────────────────────────── describe('buildClaudexBanner / buildClaudexContractNote', () => { const models = { primary: 'gpt-5.6-sol', smallFast: 'gpt-5.6-luna' }; it('banner marks EXPERIMENTAL and names the models + proxy', () => { const banner = buildClaudexBanner(models); expect(banner).toMatch(/EXPERIMENTAL/); expect(banner).toMatch(/gpt-5\.6-sol/); expect(banner).toMatch(/gpt-5\.6-luna/); expect(banner).toMatch(/claude-code-proxy/); expect(banner).not.toMatch(/unused/); // no token material in the banner }); it('contract note classifies the runtime as EXPERIMENTAL GPT-via-proxy', () => { const note = buildClaudexContractNote(models); expect(note).toMatch(/EXPERIMENTAL/); expect(note).toMatch(/gpt-5\.6-sol/); expect(note).toMatch(/not.*Anthropic/i); }); }); // ─── proxy gate ─────────────────────────────────────────────────────────────── describe('runClaudexProxyGate', () => { it('is ok when the first preflight already passes', async () => { const preflight = vi.fn().mockResolvedValue(makeReport()); const ensureProxy = vi.fn(); const reauth = vi.fn(); const gate = await runClaudexProxyGate({ preflight, ensureProxy, reauth }); expect(gate.ok).toBe(true); expect(ensureProxy).not.toHaveBeenCalled(); expect(reauth).not.toHaveBeenCalled(); }); it('fails fast when the binary is missing (no reauth, no start)', async () => { const preflight = vi .fn() .mockResolvedValue( makeReport({ binaryPresent: false, ok: false, problems: ['binary not found'] }), ); const ensureProxy = vi.fn(); const reauth = vi.fn(); const gate = await runClaudexProxyGate({ preflight, ensureProxy, reauth }); expect(gate.ok).toBe(false); expect(reauth).not.toHaveBeenCalled(); expect(ensureProxy).not.toHaveBeenCalled(); }); it('runs device reauth then re-preflights when OAuth needs it', async () => { const preflight = vi .fn() .mockResolvedValueOnce( makeReport({ auth: { state: 'expired' }, needsReauth: true, ok: false, problems: ['expired'], }), ) .mockResolvedValueOnce(makeReport()); const reauth = vi.fn().mockReturnValue(0); const gate = await runClaudexProxyGate({ preflight, reauth, ensureProxy: vi.fn() }); expect(reauth).toHaveBeenCalledTimes(1); expect(preflight).toHaveBeenCalledTimes(2); expect(gate.ok).toBe(true); }); it('does NOT reauth when auth is already valid', async () => { const preflight = vi.fn().mockResolvedValue(makeReport()); const reauth = vi.fn(); await runClaudexProxyGate({ preflight, reauth, ensureProxy: vi.fn() }); expect(reauth).not.toHaveBeenCalled(); }); it('aborts when device reauth fails', async () => { const preflight = vi.fn().mockResolvedValue( makeReport({ auth: { state: 'unauthenticated' }, needsReauth: true, ok: false, problems: ['unauth'], }), ); const reauth = vi.fn().mockReturnValue(1); const gate = await runClaudexProxyGate({ preflight, reauth, ensureProxy: vi.fn() }); expect(gate.ok).toBe(false); expect(gate.problems.join(' ')).toMatch(/re-auth/i); }); it('starts the proxy then re-preflights when nothing is live', async () => { const preflight = vi .fn() .mockResolvedValueOnce( makeReport({ live: false, listenerVerdict: 'unknown', ok: false, problems: ['dead'] }), ) .mockResolvedValueOnce(makeReport()); const ensureProxy = vi.fn().mockResolvedValue({ live: true, method: 'systemd' }); const gate = await runClaudexProxyGate({ preflight, ensureProxy, reauth: vi.fn() }); expect(ensureProxy).toHaveBeenCalledTimes(1); expect(gate.ok).toBe(true); }); it('aborts (non-sensitive) when the proxy cannot come up trusted', async () => { const preflight = vi .fn() .mockResolvedValue( makeReport({ live: false, listenerVerdict: 'unknown', ok: false, problems: ['dead'] }), ); const ensureProxy = vi.fn().mockResolvedValue({ live: false, method: 'untrusted' }); const gate = await runClaudexProxyGate({ preflight, ensureProxy, reauth: vi.fn() }); expect(gate.ok).toBe(false); expect(gate.problems.join(' ')).toMatch(/untrusted/); // Non-sensitive: no token material in surfaced problems. expect(gate.problems.join(' ')).not.toMatch(/access_token|refresh_token|sk-/); }); }); // ─── launch orchestration (fail-closed ordering) ────────────────────────────── describe('launchClaudex', () => { const baseDeps = { baseEnv: {}, proxyGate: () => Promise.resolve({ ok: true, report: makeReport(), problems: [] }), resolveConfigDir: () => '/home/agent/.config/mosaic/claudex/home', log: () => {}, errorLog: () => {}, fail: (() => { throw new Error('exit'); }) as (code: number) => never, }; it('yolo=true passes --dangerously-skip-permissions + injected env to claude', async () => { const exec = vi.fn(); await launchClaudex(['--print', 'hi'], true, okAdapter({ exec }), baseDeps); expect(exec).toHaveBeenCalledTimes(1); const [cmd, args, env] = exec.mock.calls[0]!; expect(cmd).toBe('claude'); expect(args[0]).toBe('--dangerously-skip-permissions'); expect(args).toContain('--append-system-prompt'); expect(args).toContain('--print'); expect(args).toContain('hi'); expect(env.ANTHROPIC_AUTH_TOKEN).toBe('unused'); expect(env.ANTHROPIC_BASE_URL).toBe(CLAUDEX_PROXY_URL); expect(env.CLAUDE_CONFIG_DIR).toBe('/home/agent/.config/mosaic/claudex/home'); expect(env.ANTHROPIC_MODEL).toBe('gpt-5.6-sol'); }); it('non-yolo omits --dangerously-skip-permissions but still injects the proxy env', async () => { const exec = vi.fn(); await launchClaudex([], false, okAdapter({ exec }), baseDeps); const [, args, env] = exec.mock.calls[0]!; expect(args).not.toContain('--dangerously-skip-permissions'); expect(args[0]).toBe('--append-system-prompt'); expect(env.ANTHROPIC_AUTH_TOKEN).toBe('unused'); }); it('appends the EXPERIMENTAL contract note to the composed prompt', async () => { const exec = vi.fn(); await launchClaudex([], true, okAdapter({ exec, composePrompt: () => '# BASE' }), baseDeps); const args = exec.mock.calls[0]![1] as string[]; const promptIdx = args.indexOf('--append-system-prompt') + 1; expect(args[promptIdx]).toContain('# BASE'); expect(args[promptIdx]).toMatch(/EXPERIMENTAL/); }); it('runs the harness preflight BEFORE the proxy gate and exec', async () => { const order: string[] = []; const adapter = okAdapter({ harnessPreflight: () => order.push('preflight'), exec: () => order.push('exec'), }); await launchClaudex([], true, adapter, { ...baseDeps, proxyGate: () => { order.push('gate'); return Promise.resolve({ ok: true, report: makeReport(), problems: [] }); }, }); expect(order).toEqual(['preflight', 'gate', 'exec']); }); it('FAIL CLOSED: exits WITHOUT exec when the proxy gate fails', async () => { const exec = vi.fn(); const errors: string[] = []; await expect( launchClaudex([], true, okAdapter({ exec }), { ...baseDeps, proxyGate: () => Promise.resolve({ ok: false, report: makeReport({ ok: false }), problems: ['no proxy'] }), errorLog: (m: string) => errors.push(m), }), ).rejects.toThrow('exit'); expect(exec).not.toHaveBeenCalled(); expect(errors.join('\n')).toMatch(/no proxy/); }); it('FAIL CLOSED: exits WITHOUT exec when the config-dir guard throws', async () => { const exec = vi.fn(); await expect( launchClaudex([], true, okAdapter({ exec }), { ...baseDeps, resolveConfigDir: () => { throw new Error('refusing to use ~/.claude'); }, }), ).rejects.toThrow('exit'); expect(exec).not.toHaveBeenCalled(); }); it('FAIL CLOSED: reports a non-Error throw via String() and still aborts', async () => { const exec = vi.fn(); const errors: string[] = []; await expect( launchClaudex([], true, okAdapter({ exec }), { ...baseDeps, resolveConfigDir: () => { // A non-Error throw exercises the String(err) branch of the catch. throw { toString: () => 'string-shaped failure' }; }, errorLog: (m: string) => errors.push(m), }), ).rejects.toThrow('exit'); expect(exec).not.toHaveBeenCalled(); expect(errors.join('\n')).toMatch(/string-shaped failure/); }); }); // ─── production DI defaults (fallback-branch coverage; no real proxy touched) ── describe('production dependency defaults', () => { it('assertIsolatedConfigDir defaults realClaudeDir to ~/.claude', () => { const safe = join(tmpdir(), 'mosaic-claudex-default-real', 'home'); // Only canonicalize injected; realClaudeDir falls back to ~/.claude. expect(assertIsolatedConfigDir(safe, { canonicalize: idCanon })).toBe(safe); }); it('assertIsolatedConfigDir default canonicalizer resolves a non-existent path', () => { // No canonicalize dep → exercises the real realpath-longest-ancestor walk // (including the not-yet-existing tail), on a path safely outside ~/.claude. const safe = join(tmpdir(), 'mosaic-claudex-canon', 'nested', 'home'); expect(assertIsolatedConfigDir(safe)).toContain('mosaic-claudex-canon'); }); it('resolveClaudexConfigDir defaults mosaicHome when not injected', () => { const dir = mkdtempSync(join(tmpdir(), 'claudex-cfg-')); try { const target = join(dir, 'home'); // Override env points elsewhere; mosaicHome dep omitted → MOSAIC_HOME default path is exercised. const out = resolveClaudexConfigDir( { [CLAUDEX_CONFIG_DIR_ENV]: target }, { canonicalize: idCanon }, ); expect(out).toBe(target); expect(lstatSync(target).isDirectory()).toBe(true); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('runClaudexProxyGate defaults ensureProxy/reauth/log without invoking them on a missing binary', async () => { // Only preflight injected; binary missing → returns before the default // ensureProxy/reauth thunks could ever reach the real proxy. const preflight = vi .fn() .mockResolvedValue(makeReport({ binaryPresent: false, ok: false, problems: ['missing'] })); const gate = await runClaudexProxyGate({ preflight }); expect(gate.ok).toBe(false); }); it('launchClaudex defaults log/errorLog/fail/baseEnv/models/buildEnv on the success path', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { const exec = vi.fn(); // Inject only the boundaries that would touch the real proxy/FS; let the // rest default. Success path never calls fail/errorLog. await launchClaudex([], true, okAdapter({ exec }), { proxyGate: () => Promise.resolve({ ok: true, report: makeReport(), problems: [] }), resolveConfigDir: () => join(tmpdir(), 'claudex-default-launch'), }); expect(exec).toHaveBeenCalledTimes(1); const [, , env] = exec.mock.calls[0]!; expect(env.ANTHROPIC_AUTH_TOKEN).toBe('unused'); expect(env.ANTHROPIC_MODEL).toBe(CLAUDEX_DEFAULT_PRIMARY_MODEL); } finally { logSpy.mockRestore(); } }); });