llm-complete-openrouter-integration.test.mjs
111 lines 3.9 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Tier 2 — INTEGRATION: OpenRouter lane interacting with the rest of completeChat's
3 * provider-selection logic (lib/llm-complete.mjs).
4 *
5 * Scope: how the openrouter lane composes with the other provider rules — explicit
6 * selection wins over co-present keys, and OpenRouter is NEVER selected implicitly
7 * (backward-compatibility guarantee: adding OPENROUTER_API_KEY to a deployment must not
8 * silently change the active provider). Network is mocked.
9 */
10 import { describe, it, beforeEach, afterEach } from 'node:test';
11 import assert from 'node:assert';
12 import { completeChat } from '../lib/llm-complete.mjs';
13
14 const ORIG = { ...process.env };
15 const origFetch = globalThis.fetch;
16
17 const CHAT_ENV_KEYS = [
18 'OPENAI_API_KEY',
19 'ANTHROPIC_API_KEY',
20 'DEEPINFRA_API_KEY',
21 'OPENROUTER_API_KEY',
22 'OPENROUTER_CHAT_MODEL',
23 'OPENROUTER_SITE_URL',
24 'OPENROUTER_APP_TITLE',
25 'KNOWTATION_CHAT_PROVIDER',
26 'KNOWTATION_CHAT_PREFER_ANTHROPIC',
27 ];
28
29 function clearChatEnv() {
30 for (const k of CHAT_ENV_KEYS) delete process.env[k];
31 }
32
33 function restoreEnv() {
34 for (const k of CHAT_ENV_KEYS) {
35 if (ORIG[k] === undefined) delete process.env[k];
36 else process.env[k] = ORIG[k];
37 }
38 }
39
40 /** Route by hostname; record every host that was hit. */
41 function mockByHost(handlers) {
42 const hits = [];
43 globalThis.fetch = async (url) => {
44 const u = String(url);
45 hits.push(u);
46 for (const [needle, body] of Object.entries(handlers)) {
47 if (u.includes(needle)) {
48 return { ok: true, json: async () => body };
49 }
50 }
51 return { ok: false, status: 599, text: async () => `unexpected host: ${u}` };
52 };
53 return hits;
54 }
55
56 describe('OpenRouter lane — integration with provider selection', () => {
57 beforeEach(() => {
58 clearChatEnv();
59 });
60
61 afterEach(() => {
62 globalThis.fetch = origFetch;
63 restoreEnv();
64 });
65
66 it('explicit openrouter wins even when OpenAI, Anthropic, and DeepInfra keys are all set', async () => {
67 process.env.KNOWTATION_CHAT_PROVIDER = 'openrouter';
68 process.env.OPENROUTER_API_KEY = 'or-test';
69 process.env.OPENAI_API_KEY = 'sk-openai';
70 process.env.ANTHROPIC_API_KEY = 'sk-ant';
71 process.env.DEEPINFRA_API_KEY = 'di-test';
72 const hits = mockByHost({
73 'openrouter.ai': { choices: [{ message: { content: 'from-openrouter' } }] },
74 });
75 const out = await completeChat({}, { system: 's', user: 'u' });
76 assert.strictEqual(out, 'from-openrouter');
77 assert.ok(hits.every((u) => u.includes('openrouter.ai')));
78 });
79
80 it('OPENROUTER_API_KEY alone (no explicit provider) does NOT change the default — OpenAI stays primary', async () => {
81 process.env.OPENROUTER_API_KEY = 'or-test';
82 process.env.OPENAI_API_KEY = 'sk-openai';
83 const hits = mockByHost({
84 'api.openai.com': { choices: [{ message: { content: 'from-openai-default' } }] },
85 });
86 const out = await completeChat({}, { system: 's', user: 'u' });
87 assert.strictEqual(out, 'from-openai-default');
88 assert.ok(hits.every((u) => !u.includes('openrouter.ai')));
89 });
90
91 it('OPENROUTER_API_KEY alone with no other keys still does NOT route to OpenRouter (explicit-only)', async () => {
92 process.env.OPENROUTER_API_KEY = 'or-test';
93 // No OpenAI/Anthropic/DeepInfra → default path falls through to Ollama, not OpenRouter.
94 const hits = mockByHost({
95 'localhost:11434': { message: { content: 'from-ollama' } },
96 });
97 const out = await completeChat({}, { system: 's', user: 'u' });
98 assert.strictEqual(out, 'from-ollama');
99 assert.ok(hits.every((u) => !u.includes('openrouter.ai')));
100 });
101
102 it('propagates an OpenRouter HTTP error through completeChat with status detail', async () => {
103 process.env.KNOWTATION_CHAT_PROVIDER = 'openrouter';
104 process.env.OPENROUTER_API_KEY = 'or-test';
105 globalThis.fetch = async () => ({ ok: false, status: 429, text: async () => 'rate limited' });
106 await assert.rejects(
107 () => completeChat({}, { system: 's', user: 'u' }),
108 /OpenRouter chat failed: 429/,
109 );
110 });
111 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago