llm-switcher 1.6.6 → 1.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import http from 'node:http';
7
+ import { spawn } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
11
+ const preflightScript = path.join(root, 'preflight-check.mjs');
12
+ const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
13
+
14
+ function runPreflight(tool, dir) {
15
+ return new Promise((resolve, reject) => {
16
+ const cp = spawn(process.execPath, [preflightScript, tool, dir], {
17
+ stdio: ['ignore', 'pipe', 'pipe']
18
+ });
19
+ let stdout = '';
20
+ let stderr = '';
21
+ cp.stdout.on('data', b => stdout += b);
22
+ cp.stderr.on('data', b => stderr += b);
23
+ cp.on('close', status => resolve({ status, stdout, stderr }));
24
+ cp.on('error', reject);
25
+ });
26
+ }
27
+
28
+ test('preflight-check: verifies available models, warns on dropped models and invalid keys', async t => {
29
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'llmsw-preflight-'));
30
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
31
+
32
+ let mockStatus = 200;
33
+ let mockModels = [
34
+ { id: 'zencore/space-bunny-free' },
35
+ { id: 'zencore/muse-spark-1.3-contributor-free' },
36
+ { id: 'codex/gpt-6-astra' }
37
+ ];
38
+
39
+ const mockUpstream = http.createServer((req, res) => {
40
+ if (mockStatus !== 200) {
41
+ res.statusCode = mockStatus;
42
+ return res.end('{"error":{"message":"Invalid API key"}}');
43
+ }
44
+ res.setHeader('content-type', 'application/json');
45
+ res.end(JSON.stringify({ data: mockModels }));
46
+ });
47
+ await listen(mockUpstream);
48
+ t.after(() => mockUpstream.close());
49
+
50
+ const port = mockUpstream.address().port;
51
+ const config = {
52
+ activeProfiles: { claude: 'mock-prof', codex: 'mock-prof' },
53
+ profiles: {
54
+ 'mock-prof': {
55
+ name: 'Mock Provider',
56
+ baseURL: `http://127.0.0.1:${port}/v1`,
57
+ apiKey: 'test-key',
58
+ defaultModels: {
59
+ sonnet: 'zencore/space-bunny-free',
60
+ opus: 'zencore/space-bunny-free',
61
+ haiku: 'zencore/muse-spark-1.3-contributor-free'
62
+ }
63
+ }
64
+ }
65
+ };
66
+
67
+ fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(config, null, 2));
68
+ fs.writeFileSync(path.join(dir, 'route-claude.txt'), 'claude -> mock-prof | 127.0.0.1 | zencore/space-bunny-free\n');
69
+
70
+ // Case 1: All models available -> verified ✓
71
+ const r1 = await runPreflight('claude', dir);
72
+ assert.equal(r1.status, 0);
73
+ assert.match(r1.stdout, /verified ✓/);
74
+ assert.doesNotMatch(r1.stdout, /CẢNH BÁO/);
75
+
76
+ // Case 2: Consecutive call hits cache
77
+ const r2 = await runPreflight('claude', dir);
78
+ assert.equal(r2.status, 0);
79
+ assert.match(r2.stdout, /verified ✓/);
80
+
81
+ // Clear cache for next test cases
82
+ fs.rmSync(path.join(dir, 'preflight-cache.json'), { force: true });
83
+
84
+ // Case 3: One model was dropped/deprecated (e.g. haiku set to dropped-model)
85
+ config.profiles['mock-prof'].defaultModels.haiku = 'zencore/old-dropped-model';
86
+ fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(config, null, 2));
87
+
88
+ const r3 = await runPreflight('claude', dir);
89
+ assert.equal(r3.status, 0);
90
+ assert.match(r3.stdout, /CẢNH BÁO/);
91
+ assert.match(r3.stdout, /old-dropped-model/);
92
+ assert.match(r3.stdout, /slot haiku/);
93
+ assert.match(r3.stdout, /switch ui/);
94
+
95
+ fs.rmSync(path.join(dir, 'preflight-cache.json'), { force: true });
96
+
97
+ // Case 4: API key rejected (401)
98
+ mockStatus = 401;
99
+ const r4 = await runPreflight('claude', dir);
100
+ assert.equal(r4.status, 0);
101
+ assert.match(r4.stdout, /API key cho profile 'mock-prof' không hợp lệ/);
102
+ assert.match(r4.stdout, /HTTP 401/);
103
+
104
+ fs.rmSync(path.join(dir, 'preflight-cache.json'), { force: true });
105
+
106
+ // Case 5: Endpoint unreachable (bad port)
107
+ config.profiles['mock-prof'].baseURL = 'http://127.0.0.1:1';
108
+ fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(config, null, 2));
109
+
110
+ const r5 = await runPreflight('claude', dir);
111
+ assert.equal(r5.status, 0);
112
+ assert.match(r5.stdout, /Không thể kết nối tới endpoint/);
113
+
114
+ // Case 6: Tool is off (no route file) -> exits quietly
115
+ fs.rmSync(path.join(dir, 'route-claude.txt'), { force: true });
116
+ const r6 = await runPreflight('claude', dir);
117
+ assert.equal(r6.status, 0);
118
+ assert.equal(r6.stdout.trim(), '');
119
+ });
package/tests/ui.test.mjs CHANGED
@@ -115,3 +115,38 @@ test('the 1M switch is gone from the slots, the card and the payload', () => {
115
115
  assert.doesNotMatch(functionBody('collectCurrentSlotValues'), /model1M\s*[,}]/);
116
116
  assert.doesNotMatch(html, /profilePayload\.model1M/);
117
117
  });
118
+
119
+ test('documentation tab is properly integrated with sidebar navigation, sections, and helpers', () => {
120
+ // Sidebar navigation link
121
+ assert.match(html, /<a class="nav-link" href="#\/docs" data-nav="docs"/);
122
+ // View panel exists
123
+ assert.match(html, /<div id="view-docs" class="view-panel"/);
124
+ // Table of Contents navigation exists
125
+ assert.match(html, /<nav class="docs-toc" id="docsToc"/);
126
+
127
+ // All 10 core sections exist
128
+ const expectedSections = [
129
+ 'doc-overview',
130
+ 'doc-install',
131
+ 'doc-cli',
132
+ 'doc-blindfold',
133
+ 'doc-agy',
134
+ 'doc-optimizers',
135
+ 'doc-agent-mcp',
136
+ 'doc-advanced',
137
+ 'doc-config',
138
+ 'doc-faq'
139
+ ];
140
+ for (const s of expectedSections) {
141
+ assert.match(html, new RegExp(`id="${s}"`), `Missing section ${s} in ui.html`);
142
+ assert.match(html, new RegExp(`href="#${s}"`), `Missing TOC link for ${s} in ui.html`);
143
+ }
144
+
145
+ // Hash router handles 'docs'
146
+ assert.match(html, /validRoutes\s*=\s*\[[^\]]*'docs'[^\]]*\]/);
147
+ assert.match(html, /activeRoute === 'docs'/);
148
+
149
+ // Helper functions exist
150
+ assert.match(html, /function copyDocCode\(/);
151
+ assert.match(html, /function initDocsScrollSpy\(/);
152
+ });