import { describe, it, expect, vi } from 'vitest'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { CLAUDEX_PROXY_HOST, CLAUDEX_PROXY_PORT, CLAUDEX_PROXY_URL, CLAUDEX_PROXY_BINARY, CLAUDEX_HEALTH_PATH, CLAUDEX_HEALTH_URL, buildAuthStatusArgs, buildDeviceAuthArgs, buildServeArgs, parseAuthStatus, checkProxyBinary, checkAuthStatus, runDeviceReauth, probeLiveness, buildSystemdUnitContent, systemdUnitPath, installSystemdUnit, startNohupProxy, verifyListenerIdentity, runProxyPreflight, ensureProxyRunning, type AuthStatus, type ProxyRunResult, type SpawnedChild, type ListenerIdentity, } from './claudex-proxy.js'; /** * P1 — Proxy preflight + lifecycle helpers for `mosaic yolo claudex`. * * Security-relevant invariants exercised here: * - Liveness probe hits the proxy's dedicated `GET /healthz` and treats only a * 2xx as "alive" — a *proxy-specific* health contract, not arbitrary HTTP on * the port (CWE-345: a local port-squatter must not be trusted as the proxy). * This also honors spec gotcha #1 (never `curl -f` the root, which returns * non-2xx): `/healthz` returns 2xx when the proxy is up, so a healthy proxy is * never mistaken for dead and no duplicate proxy is spawned. * - Auth-status parsing NEVER surfaces OAuth token material — only a coarse * state + optional expiry — even if a token-shaped string appears in output. * - The systemd unit's ExecStart never interpolates an unvalidated path * (CWE-74: a CR/LF in the path could inject arbitrary systemd directives). * - The nohup fallback captures spawn's *async* error event instead of crashing. */ describe('claudex-proxy constants', () => { it('pins the proxy endpoint to loopback :18765 (spec table)', () => { expect(CLAUDEX_PROXY_HOST).toBe('127.0.0.1'); expect(CLAUDEX_PROXY_PORT).toBe(18765); expect(CLAUDEX_PROXY_URL).toBe('http://127.0.0.1:18765'); expect(CLAUDEX_PROXY_BINARY).toBe('claude-code-proxy'); }); it('exposes the dedicated /healthz liveness endpoint (not the root path)', () => { expect(CLAUDEX_HEALTH_PATH).toBe('/healthz'); expect(CLAUDEX_HEALTH_URL).toBe('http://127.0.0.1:18765/healthz'); }); it('builds the documented codex subcommand argv', () => { expect(buildAuthStatusArgs()).toEqual(['codex', 'auth', 'status']); expect(buildDeviceAuthArgs()).toEqual(['codex', 'auth', 'device']); expect(buildServeArgs()).toEqual(['serve', '--no-monitor']); }); }); describe('parseAuthStatus', () => { it('reports valid on exit 0 with an authenticated marker', () => { const s = parseAuthStatus({ status: 0, stdout: 'Authenticated as user; token valid', stderr: '', }); expect(s.state).toBe('valid'); }); it('reports expired when output mentions expiry', () => { const s = parseAuthStatus({ status: 0, stdout: 'Token expired 2 days ago', stderr: '' }); expect(s.state).toBe('expired'); }); it('reports unauthenticated when output says not logged in', () => { const s = parseAuthStatus({ status: 1, stdout: '', stderr: 'not authenticated: run codex auth device', }); expect(s.state).toBe('unauthenticated'); }); it('reports unknown on an unrecognized non-zero exit', () => { const s = parseAuthStatus({ status: 2, stdout: 'weird', stderr: '' }); expect(s.state).toBe('unknown'); }); it('does NOT trust a signal-terminated check (status null) even with an auth-looking line', () => { // status: null means the process was killed by a signal — an INCOMPLETE // check. An auth-looking line that happened to be flushed must not be read // as valid, or preflight passes on a check that never finished. const s = parseAuthStatus({ status: null, stdout: 'Authenticated', stderr: '' }); expect(s.state).toBe('unknown'); }); it('extracts a best-effort expiry in days when present', () => { const s = parseAuthStatus({ status: 0, stdout: 'Authenticated; expires in 9 days', stderr: '', }); expect(s.state).toBe('valid'); expect(s.expiresInDays).toBe(9); }); it('treats a clean exit 0 with no explicit markers as valid', () => { const s = parseAuthStatus({ status: 0, stdout: 'Session active for account foo', stderr: '' }); expect(s.state).toBe('valid'); expect(s.expiresInDays).toBeUndefined(); }); it('NEVER retains token-shaped material from output', () => { const leaky = 'Authenticated. access_token=sk-abc123SECRETdeadbeef refresh_token=rt-9999'; const s: AuthStatus = parseAuthStatus({ status: 0, stdout: leaky, stderr: '' }); const serialized = JSON.stringify(s); expect(serialized).not.toContain('sk-abc123SECRETdeadbeef'); expect(serialized).not.toContain('rt-9999'); expect(serialized).not.toContain('access_token'); expect(serialized).not.toContain('refresh_token'); }); }); describe('checkAuthStatus', () => { it('runs the status subcommand and parses the result', () => { const run = vi.fn( (_cmd: string, _args: string[]): ProxyRunResult => ({ status: 0, stdout: 'Authenticated; expires in 7 days', stderr: '', }), ); const s = checkAuthStatus(run); expect(run).toHaveBeenCalledWith(CLAUDEX_PROXY_BINARY, ['codex', 'auth', 'status']); expect(s.state).toBe('valid'); expect(s.expiresInDays).toBe(7); }); it('surfaces unknown when the default runner cannot find the binary', () => { // Exercises the default spawnSync path against an absent binary: no throw, // status is non-zero/null → unknown. Deterministic on a box without the proxy. const s = checkAuthStatus(); expect(['unknown', 'unauthenticated', 'valid', 'expired']).toContain(s.state); }); }); describe('runDeviceReauth', () => { it('spawns the device flow with inherited stdio (never captures the code/token)', () => { const calls: Array<{ cmd: string; args: string[]; opts: { stdio: string } }> = []; const status = runDeviceReauth((cmd, args, opts) => { calls.push({ cmd, args, opts }); return { status: 0 }; }); expect(status).toBe(0); expect(calls).toHaveLength(1); expect(calls[0]!.cmd).toBe(CLAUDEX_PROXY_BINARY); expect(calls[0]!.args).toEqual(['codex', 'auth', 'device']); // stdio 'inherit' is the security-critical bit: the device code streams to // the user's TTY; the launcher never pipes/captures it. expect(calls[0]!.opts.stdio).toBe('inherit'); }); it('returns 1 when the child yields no status (absent binary)', () => { const status = runDeviceReauth(() => ({ status: null })); expect(status).toBe(1); }); }); describe('checkProxyBinary', () => { it('resolves via the default `which` path (proxy absent → null)', () => { // Covers the default resolver; on CI/dev the proxy is not installed. const r = checkProxyBinary(); expect(typeof r.present).toBe('boolean'); if (!r.present) expect(r.path).toBeNull(); }); it('reports present with the resolved path', () => { const r = checkProxyBinary(() => '/home/u/.local/bin/claude-code-proxy'); expect(r.present).toBe(true); expect(r.path).toBe('/home/u/.local/bin/claude-code-proxy'); }); it('reports absent when the resolver finds nothing', () => { const r = checkProxyBinary(() => null); expect(r.present).toBe(false); expect(r.path).toBeNull(); }); }); describe('probeLiveness (proxy-specific /healthz, not arbitrary HTTP)', () => { it('defaults to probing the /healthz endpoint, never the root path', async () => { const seen: string[] = []; await probeLiveness(undefined, async (u) => { seen.push(u); return { status: 200 }; }); expect(seen[0]).toBe(CLAUDEX_HEALTH_URL); expect(seen[0]).toContain('/healthz'); }); it('treats a 200 on /healthz as alive', async () => { const live = await probeLiveness(CLAUDEX_HEALTH_URL, async () => ({ status: 200 })); expect(live).toBe(true); }); it('treats a 204 on /healthz as alive', async () => { const live = await probeLiveness(CLAUDEX_HEALTH_URL, async () => ({ status: 204 })); expect(live).toBe(true); }); it('treats a 404 as DEAD — does not trust an arbitrary responder on the port (CWE-345)', async () => { // The whole point: a random local process squatting :18765 will not honor the // proxy's /healthz contract, so a non-2xx there must not be mistaken for the proxy. const live = await probeLiveness(CLAUDEX_HEALTH_URL, async () => ({ status: 404 })); expect(live).toBe(false); }); it('treats a 500 as DEAD (unhealthy / not the proxy health contract)', async () => { const live = await probeLiveness(CLAUDEX_HEALTH_URL, async () => ({ status: 500 })); expect(live).toBe(false); }); it('treats a missing status as dead', async () => { const live = await probeLiveness(CLAUDEX_HEALTH_URL, async () => ({})); expect(live).toBe(false); }); it('treats a connection failure (reject) as dead', async () => { const live = await probeLiveness(CLAUDEX_HEALTH_URL, async () => { throw new Error('ECONNREFUSED'); }); expect(live).toBe(false); }); it('treats a timeout as dead', async () => { const never = () => new Promise<{ status?: number }>(() => {}); const live = await probeLiveness(CLAUDEX_HEALTH_URL, never, 20); expect(live).toBe(false); }); }); describe('buildSystemdUnitContent', () => { it('emits a user unit that execs the given binary with serve args', () => { const unit = buildSystemdUnitContent('/home/u/.local/bin/claude-code-proxy'); expect(unit).toContain('[Unit]'); expect(unit).toContain('[Service]'); expect(unit).toContain('[Install]'); expect(unit).toContain('/home/u/.local/bin/claude-code-proxy serve --no-monitor'); expect(unit).toContain('WantedBy=default.target'); }); it('never embeds credential material', () => { const unit = buildSystemdUnitContent('/home/u/.local/bin/claude-code-proxy'); expect(unit).not.toMatch(/token/i); expect(unit).not.toMatch(/auth\.json/i); }); it('rejects a path containing a newline (CWE-74 systemd directive injection)', () => { // A raw newline in ExecStart would let an attacker append arbitrary unit // directives — e.g. `ExecStartPost=curl evil`. Must be rejected outright. expect(() => buildSystemdUnitContent('/bin/claude-code-proxy\nExecStartPost=/bin/rm -rf /'), ).toThrow(); }); it('rejects a path containing a carriage return', () => { expect(() => buildSystemdUnitContent('/bin/claude-code-proxy\rmalicious')).toThrow(); }); it('rejects a path with other control characters', () => { expect(() => buildSystemdUnitContent('/bin/claude-code-proxy\x00nul')).toThrow(); }); it('rejects a non-absolute path', () => { expect(() => buildSystemdUnitContent('claude-code-proxy')).toThrow(); expect(() => buildSystemdUnitContent('')).toThrow(); }); it('systemd-quotes a path that contains spaces', () => { const unit = buildSystemdUnitContent('/home/u/my apps/claude-code-proxy'); expect(unit).toContain('ExecStart="/home/u/my apps/claude-code-proxy" serve --no-monitor'); }); it('escapes embedded quotes and backslashes when quoting', () => { const unit = buildSystemdUnitContent('/home/u/we"ird\\dir/claude-code-proxy'); // No unescaped closing quote can terminate the token early. expect(unit).toContain('ExecStart="/home/u/we\\"ird\\\\dir/claude-code-proxy" serve'); }); it('leaves a clean absolute path unquoted (no needless churn)', () => { const unit = buildSystemdUnitContent('/home/u/.local/bin/claude-code-proxy'); expect(unit).toContain('ExecStart=/home/u/.local/bin/claude-code-proxy serve --no-monitor'); }); }); describe('systemdUnitPath', () => { it('targets the systemd --user unit dir', () => { expect(systemdUnitPath('/home/u')).toBe( '/home/u/.config/systemd/user/claude-code-proxy.service', ); }); }); describe('installSystemdUnit', () => { it('writes the unit and returns true when daemon-reload succeeds', () => { let written: { path: string; content: string } | null = null; const ok = installSystemdUnit('/bin/claude-code-proxy', { home: '/home/u', writeUnit: (path, content) => { written = { path, content }; }, run: () => ({ status: 0, stdout: '', stderr: '' }), }); expect(ok).toBe(true); expect(written).not.toBeNull(); expect(written!.path).toBe('/home/u/.config/systemd/user/claude-code-proxy.service'); expect(written!.content).toContain('ExecStart=/bin/claude-code-proxy serve --no-monitor'); }); it('returns false when daemon-reload fails (systemd --user unavailable)', () => { const ok = installSystemdUnit('/bin/claude-code-proxy', { home: '/home/u', writeUnit: () => {}, run: () => ({ status: 1, stdout: '', stderr: 'Failed to connect to bus' }), }); expect(ok).toBe(false); }); it('returns false when writing the unit throws', () => { const ok = installSystemdUnit('/bin/claude-code-proxy', { home: '/home/u', writeUnit: () => { throw new Error('EACCES'); }, run: () => ({ status: 0, stdout: '', stderr: '' }), }); expect(ok).toBe(false); }); it('refuses to write a unit for an injection-bearing path (never writes a poisoned unit)', () => { const writeUnit = vi.fn(); const ok = installSystemdUnit('/bin/claude-code-proxy\nExecStartPost=/bin/rm -rf /', { home: '/home/u', writeUnit, run: () => ({ status: 0, stdout: '', stderr: '' }), }); expect(ok).toBe(false); // The poisoned unit content is never even produced, so nothing is written. expect(writeUnit).not.toHaveBeenCalled(); }); it('writes to a real temp dir via the default writer', () => { const home = mkdtempSync(join(tmpdir(), 'claudex-unit-')); try { const ok = installSystemdUnit('/bin/claude-code-proxy', { home, run: () => ({ status: 0, stdout: '', stderr: '' }), }); expect(ok).toBe(true); const written = readFileSync(systemdUnitPath(home), 'utf8'); expect(written).toContain('[Service]'); } finally { rmSync(home, { recursive: true, force: true }); } }); }); describe('runProxyPreflight', () => { const trustedListener = () => 'ok' as const; it('is ok when binary present, auth valid, proxy live, and listener identity-verified', async () => { const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'valid' }), probe: async () => true, verifyListener: trustedListener, }); expect(report.ok).toBe(true); expect(report.listenerVerdict).toBe('ok'); expect(report.problems).toEqual([]); }); it('flags a missing binary', async () => { const report = await runProxyPreflight({ checkBinary: () => ({ present: false, path: null }), checkAuth: () => ({ state: 'valid' }), probe: async () => true, verifyListener: trustedListener, }); expect(report.ok).toBe(false); expect(report.problems.some((p) => /binary/i.test(p))).toBe(true); }); it('flags expired auth (re-auth needed)', async () => { const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'expired' }), probe: async () => true, verifyListener: trustedListener, }); expect(report.ok).toBe(false); expect(report.needsReauth).toBe(true); expect(report.problems.some((p) => /auth/i.test(p))).toBe(true); }); it('flags a dead proxy', async () => { const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'valid' }), probe: async () => false, verifyListener: trustedListener, }); expect(report.ok).toBe(false); expect(report.live).toBe(false); }); it('flags an unknown auth state without marking it for re-auth', async () => { const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'unknown' }), probe: async () => true, verifyListener: trustedListener, }); expect(report.ok).toBe(false); expect(report.needsReauth).toBe(false); expect(report.problems.some((p) => /could not determine/i.test(p))).toBe(true); }); it('does NOT pass preflight when the live responder fails identity verification (F2b)', async () => { // A squatter answering /healthz-2xx must not yield ok:true just because the // binary is installed and OAuth is valid — the identity gate holds here too. for (const verdict of ['foreign-user', 'wrong-exe', 'unknown'] as const) { const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'valid' }), probe: async () => true, verifyListener: () => verdict, }); expect(report.ok).toBe(false); expect(report.live).toBe(true); expect(report.listenerVerdict).toBe(verdict); expect(report.problems.some((p) => /identity could not be verified/i.test(p))).toBe(true); // The identity problem is non-sensitive: port + verdict only, no token. expect(JSON.stringify(report)).not.toMatch(/token|sk-|auth\.json/i); } }); it('does not verify listener identity when the proxy is dead (no listener to trust)', async () => { const verifyListener = vi.fn(() => 'ok' as const); const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'valid' }), probe: async () => false, verifyListener, }); expect(verifyListener).not.toHaveBeenCalled(); expect(report.listenerVerdict).toBe('unknown'); expect(report.ok).toBe(false); }); it('does not leak token material for any auth state', async () => { const report = await runProxyPreflight({ checkBinary: () => ({ present: true, path: '/bin/claude-code-proxy' }), checkAuth: () => ({ state: 'expired' }), probe: async () => false, verifyListener: trustedListener, }); expect(JSON.stringify(report)).not.toMatch(/token|sk-|auth\.json/i); }); it('runs end-to-end with all real defaults (no proxy installed → not ok)', async () => { // Exercises the default checkBinary/checkAuth/probe closures against a box // with no proxy: absent binary, spawnSync status, real loopback probe that // fast-fails with ECONNREFUSED. Asserts shape only (never token material). const report = await runProxyPreflight(); expect(typeof report.ok).toBe('boolean'); expect(Array.isArray(report.problems)).toBe(true); expect(['valid', 'expired', 'unauthenticated', 'unknown']).toContain(report.auth.state); expect(JSON.stringify(report)).not.toMatch(/access_token|refresh_token|sk-/i); }); }); /** * A minimal fake ChildProcess for the nohup-fallback tests: records once() * handlers so a test can drive the async 'spawn'/'error' events, and tracks * whether the 'error' listener was already attached at the moment unref() ran * (the security-critical ordering from finding #1). */ function fakeChild() { const handlers: Record void> = {}; const state = { unreffed: false, errorHandlerAtUnref: false }; const child = { once(event: string, listener: (arg?: unknown) => void) { handlers[event] = listener; return child; }, unref() { state.unreffed = true; state.errorHandlerAtUnref = typeof handlers.error === 'function'; }, emit(event: string, arg?: unknown) { handlers[event]?.(arg); }, }; return { child: child as unknown as SpawnedChild & { emit(e: string, a?: unknown): void }, state, }; } describe('startNohupProxy (finding #1 — async spawn error must not crash)', () => { it('resolves status 0 only after a confirmed spawn, and unrefs the child', async () => { const { child, state } = fakeChild(); const spawnImpl = vi.fn((_cmd: string, _args: string[]) => { queueMicrotask(() => child.emit('spawn')); return child; }); const r = await startNohupProxy({ resolveBin: () => '/bin/claude-code-proxy', spawnImpl }); expect(r.status).toBe(0); expect(state.unreffed).toBe(true); // The error listener MUST be registered before unref(), so an ENOENT that // arrives asynchronously can never become an unhandled 'error' crash. expect(state.errorHandlerAtUnref).toBe(true); expect(spawnImpl).toHaveBeenCalledWith('/bin/claude-code-proxy', ['serve', '--no-monitor'], { detached: true, stdio: 'ignore', }); }); it('captures an async spawn error (ENOENT) as a failed start instead of crashing', async () => { const { child, state } = fakeChild(); const spawnImpl = () => { queueMicrotask(() => child.emit('error', new Error('spawn claude-code-proxy ENOENT'))); return child; }; const r = await startNohupProxy({ resolveBin: () => '/bin/claude-code-proxy', spawnImpl }); expect(r.status).toBe(1); expect(r.stderr).toContain('ENOENT'); expect(state.unreffed).toBe(false); // never unref a child that failed to start }); it('captures a synchronous spawn throw as a failed start', async () => { const spawnImpl = () => { throw new Error('EACCES'); }; const r = await startNohupProxy({ resolveBin: () => '/bin/claude-code-proxy', spawnImpl }); expect(r.status).toBe(1); expect(r.stderr).toContain('EACCES'); }); it('ignores a late error after a successful spawn (settles once)', async () => { const { child } = fakeChild(); const spawnImpl = () => { queueMicrotask(() => { child.emit('spawn'); child.emit('error', new Error('late boom')); }); return child; }; const r = await startNohupProxy({ resolveBin: () => '/bin/claude-code-proxy', spawnImpl }); expect(r.status).toBe(0); // first settle wins; the late error cannot flip it }); }); describe('verifyListenerIdentity (finding #2 — OS-level listener identity, CWE-345)', () => { const me: ListenerIdentity = { pid: 4242, uid: 1000, exePath: '/home/me/.local/bin/claude-code-proxy', }; // Identity canonicalize for tests: fake paths don't exist on disk, so we map // each path to itself and exercise symlink resolution explicitly where needed. const idc = (p: string) => p; it('accepts a listener owned by the current uid whose exe is the expected proxy path', () => { const verdict = verifyListenerIdentity({ identify: () => me, currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('ok'); }); it('resolves symlinks on BOTH sides before comparing (canonical match → ok)', () => { // The listener exe and our resolved binary reach the same real file via // different symlink paths — a canonical comparison must accept it. const canon: Record = { '/var/run/proxy.link': '/opt/proxy/bin/claude-code-proxy', '/home/me/.local/bin/claude-code-proxy': '/opt/proxy/bin/claude-code-proxy', }; const verdict = verifyListenerIdentity({ identify: () => ({ ...me, exePath: '/var/run/proxy.link' }), currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: (p) => canon[p] ?? null, }); expect(verdict).toBe('ok'); }); it('does NOT trust a same-uid process at the WRONG path with the right basename (F2a)', () => { // The squatter vector on a shared-uid host: right basename, wrong path. The // basename must NEVER be a trust signal when an expected exact path resolved. const verdict = verifyListenerIdentity({ identify: () => ({ ...me, exePath: '/tmp/claude-code-proxy' }), currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('wrong-exe'); }); it('fails closed (unknown) when our own proxy binary path cannot be resolved (F2a)', () => { // No expected path → we cannot assert identity → refuse to trust (no basename // acceptance). Previously this returned `ok` by basename; that was a bypass. const verdict = verifyListenerIdentity({ identify: () => me, currentUid: () => 1000, expectedExe: () => null, canonicalize: idc, }); expect(verdict).toBe('unknown'); }); it('fails closed (unknown) when a path cannot be canonicalized (F2a)', () => { const verdict = verifyListenerIdentity({ identify: () => me, currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: () => null, // e.g. binary deleted out from under the listener }); expect(verdict).toBe('unknown'); }); it('rejects a listener owned by a DIFFERENT uid (foreign-user) — fail closed', () => { const verdict = verifyListenerIdentity({ identify: () => ({ ...me, uid: 0 }), currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('foreign-user'); }); it('rejects a same-user listener whose exe is NOT the proxy (wrong-exe)', () => { const verdict = verifyListenerIdentity({ identify: () => ({ ...me, exePath: '/usr/bin/nc' }), currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('wrong-exe'); }); it('returns unknown (fail closed) when the listener cannot be identified', () => { const verdict = verifyListenerIdentity({ identify: () => null, currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('unknown'); }); it('returns unknown when the current uid is unavailable (non-posix)', () => { const verdict = verifyListenerIdentity({ identify: () => me, currentUid: () => -1, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('unknown'); }); it('returns unknown when the listener exe path cannot be read', () => { const verdict = verifyListenerIdentity({ identify: () => ({ ...me, exePath: null }), currentUid: () => 1000, expectedExe: () => '/home/me/.local/bin/claude-code-proxy', canonicalize: idc, }); expect(verdict).toBe('unknown'); }); it('runs with real defaults without throwing (identity may be unresolved → verdict)', () => { const verdict = verifyListenerIdentity(); expect(['ok', 'foreign-user', 'wrong-exe', 'unknown']).toContain(verdict); }); }); describe('ensureProxyRunning', () => { const ok: ProxyRunResult = { status: 0, stdout: '', stderr: '' }; const nohupOk = async (): Promise => ok; const trusted = () => 'ok' as const; it('is a no-op when the proxy is already live AND identity-verified', async () => { const startSystemd = vi.fn(() => ok); const startNohup = vi.fn(nohupOk); const r = await ensureProxyRunning({ probe: async () => true, verifyListener: trusted, startSystemd, startNohup, waitMs: async () => {}, }); expect(r.method).toBe('already'); expect(r.live).toBe(true); expect(startSystemd).not.toHaveBeenCalled(); expect(startNohup).not.toHaveBeenCalled(); }); it('fails closed as untrusted when a responder holds :18765 but identity is NOT ours', async () => { // A foreign process answers /healthz but the listener is not our proxy // (foreign uid / wrong exe / unidentifiable). We must NOT trust it and must // NOT start a second proxy (the port is already taken) — fail closed. const startSystemd = vi.fn(() => ok); const startNohup = vi.fn(nohupOk); const r = await ensureProxyRunning({ probe: async () => true, verifyListener: () => 'foreign-user', startSystemd, startNohup, waitMs: async () => {}, }); expect(r.method).toBe('untrusted'); expect(r.live).toBe(false); expect(startSystemd).not.toHaveBeenCalled(); expect(startNohup).not.toHaveBeenCalled(); }); it('starts via systemd when available and then becomes trusted-live', async () => { let calls = 0; const r = await ensureProxyRunning({ probe: async () => calls++ > 0, // dead first, live after start verifyListener: trusted, startSystemd: () => ok, startNohup: async () => { throw new Error('should not fall back'); }, waitMs: async () => {}, }); expect(r.method).toBe('systemd'); expect(r.live).toBe(true); }); it('waits past a slow systemd bind before giving up (finding #2 — no duplicate proxy)', async () => { // systemd `start` returns 0 (job accepted) but the socket only binds on the // 4th probe — still well within the startup deadline. nohup must NOT run, // or two proxies would contend for :18765. let probes = 0; const startNohup = vi.fn(nohupOk); const r = await ensureProxyRunning({ probe: async () => probes++ >= 3, verifyListener: trusted, startSystemd: () => ok, startNohup, waitMs: async () => {}, settleMs: 10, startupDeadlineMs: 200, }); expect(r.method).toBe('systemd'); expect(r.live).toBe(true); expect(startNohup).not.toHaveBeenCalled(); }); it('does NOT fall back to nohup after systemd accepts but never binds (finding #1 — dup-proxy race)', async () => { // systemctl start exit 0 means the job was ACCEPTED, not bound. If it binds // just after our deadline (or systemd restarts it), a nohup fallback would // create a SECOND proxy contending for :18765. Once systemd has accepted the // job we never spawn nohup — we report a managed-service startup failure. const startNohup = vi.fn(nohupOk); const r = await ensureProxyRunning({ probe: async () => false, // never becomes live within the deadline verifyListener: trusted, startSystemd: () => ok, startNohup, waitMs: async () => {}, settleMs: 10, startupDeadlineMs: 30, }); expect(startNohup).not.toHaveBeenCalled(); expect(r.method).toBe('failed'); expect(r.live).toBe(false); }); it('does NOT trust a systemd-started responder whose identity cannot be verified', async () => { // Dead at first (so we reach the systemd start), then the socket binds — but // identity never verifies (e.g. a squatter beat systemd to the port). A live // responder that fails identity must never be reported as a successful start. let calls = 0; const startNohup = vi.fn(nohupOk); const r = await ensureProxyRunning({ probe: async () => calls++ > 0, verifyListener: () => 'wrong-exe', startSystemd: () => ok, startNohup, waitMs: async () => {}, settleMs: 10, startupDeadlineMs: 30, }); expect(startNohup).not.toHaveBeenCalled(); expect(r.method).toBe('failed'); expect(r.live).toBe(false); }); it('falls back to nohup only when systemd start FAILS outright (not accepted)', async () => { let calls = 0; const r = await ensureProxyRunning({ // A failed systemd start skips its post-start poll, so probes are: // #0 initial (dead), #1 after nohup (live). nohup fallback is reachable // ONLY because systemd never accepted the job (status 1). probe: async () => calls++ > 0, verifyListener: trusted, startSystemd: () => ({ status: 1, stdout: '', stderr: 'no systemd' }), startNohup: nohupOk, waitMs: async () => {}, settleMs: 10, startupDeadlineMs: 30, }); expect(r.method).toBe('nohup'); expect(r.live).toBe(true); }); it('does NOT trust a nohup-started responder whose identity cannot be verified', async () => { let calls = 0; const r = await ensureProxyRunning({ probe: async () => calls++ > 0, verifyListener: () => 'unknown', startSystemd: () => ({ status: 1, stdout: '', stderr: 'no systemd' }), startNohup: nohupOk, waitMs: async () => {}, settleMs: 10, startupDeadlineMs: 30, }); expect(r.method).toBe('failed'); expect(r.live).toBe(false); }); it('reports failed when nothing brings the proxy up', async () => { const r = await ensureProxyRunning({ probe: async () => false, verifyListener: trusted, startSystemd: () => ({ status: 1, stdout: '', stderr: '' }), startNohup: async () => ({ status: 1, stdout: '', stderr: '' }), waitMs: async () => {}, settleMs: 10, startupDeadlineMs: 30, }); expect(r.method).toBe('failed'); expect(r.live).toBe(false); }); });