import { test } from 'node:test'; import assert from 'node:assert/strict'; import { chmod, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { chat } from '../src/lib/router.js'; test('review router client disables model fallbacks explicitly', async () => { const originalFetch = globalThis.fetch; let body; globalThis.fetch = async (_url, init) => { body = JSON.parse(init.body); return Response.json({ message: { content: '{"axes":{},"overall":90,"grade":"A","findings":[]}' }, _shre: { model: 'ollama-remote/qwen2.5-coder:7b' } }); }; try { await chat({ routerUrl: 'http://router.test', model: 'ollama-remote/qwen2.5-coder:7b', messages: [{ role: 'user', content: 'review' }], agentId: 'code-reviewer', sessionId: 'review-test', tenantId: 'nirlab', timeoutMs: 1000 }); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(body.fallbackModels, []); assert.equal(body.tools, false); assert.equal(body.raw, true); assert.equal(body.stream, false); }); test('claude-cli models bypass the router and use subscription credentials', async () => { const dir = await mkdtemp(join(tmpdir(), 'granthi-review-claude-')); const fakeClaude = join(dir, 'claude'); await writeFile(fakeClaude, `#!/bin/sh case " $* " in *" --model claude-sonnet-4-6 "*) ;; *) exit 21 ;; esac case " $* " in *" --tools --permission-mode dontAsk --no-session-persistence "*) ;; *) exit 22 ;; esac [ -z "\${ANTHROPIC_API_KEY:-}" ] || exit 23 [ -z "\${ANTHROPIC_TOKEN:-}" ] || exit 24 printf '%s' '{"result":"{\\"axes\\":{},\\"overall\\":90,\\"grade\\":\\"A\\",\\"findings\\":[]}"}' `); await chmod(fakeClaude, 0o700); const originalFetch = globalThis.fetch; const originalApiKey = process.env.ANTHROPIC_API_KEY; const originalToken = process.env.ANTHROPIC_TOKEN; globalThis.fetch = async () => { throw new Error('router must not be called'); }; process.env.ANTHROPIC_API_KEY = 'test-api-key'; process.env.ANTHROPIC_TOKEN = 'test-token'; try { const result = await chat({ routerUrl: 'http://router.test', model: 'claude-cli/claude-sonnet-4-6', messages: [{ role: 'user', content: 'review this diff' }], agentId: 'code-reviewer', sessionId: 'review-test', tenantId: 'nirlab', timeoutMs: 1000, claudeBin: fakeClaude }); assert.match(result.content, /"overall":90/); assert.equal(result.servedModel, 'claude-cli/claude-sonnet-4-6'); } finally { globalThis.fetch = originalFetch; if (originalApiKey === undefined) delete process.env.ANTHROPIC_API_KEY; else process.env.ANTHROPIC_API_KEY = originalApiKey; if (originalToken === undefined) delete process.env.ANTHROPIC_TOKEN; else process.env.ANTHROPIC_TOKEN = originalToken; } }); test('claude-cli failures do not leak the review prompt', async () => { const dir = await mkdtemp(join(tmpdir(), 'granthi-review-claude-error-')); const fakeClaude = join(dir, 'claude'); await writeFile(fakeClaude, '#!/bin/sh\nprintf "%s\\n" "$*" >&2\nexit 25\n'); await chmod(fakeClaude, 0o700); await assert.rejects( chat({ routerUrl: 'http://router.test', model: 'claude-cli/claude-sonnet-4-6', messages: [{ role: 'user', content: 'SECRET_REVIEW_DIFF_MARKER' }], timeoutMs: 1000, claudeBin: fakeClaude }), (error) => { assert.match(error.message, /claude-cli failed/); assert.doesNotMatch(error.message, /SECRET_REVIEW_DIFF_MARKER/); return true; } ); });