codexmate 0.0.9 → 0.0.12

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.
Files changed (63) hide show
  1. package/README.md +25 -6
  2. package/README.zh-CN.md +25 -6
  3. package/package.json +53 -36
  4. package/res/logo.png +0 -0
  5. package/{cli.js → src/cli.js} +822 -327
  6. package/src/lib/cli-file-utils.js +151 -0
  7. package/src/lib/cli-models-utils.js +152 -0
  8. package/src/lib/cli-network-utils.js +148 -0
  9. package/src/lib/cli-session-utils.js +121 -0
  10. package/src/lib/cli-utils.js +139 -0
  11. package/src/res/json5.min.js +1 -0
  12. package/src/res/logo.png +0 -0
  13. package/src/res/screenshot.png +0 -0
  14. package/src/res/vue.global.js +18552 -0
  15. package/src/web-ui/app.js +2970 -0
  16. package/src/web-ui/index.html +1310 -0
  17. package/src/web-ui/logic.mjs +157 -0
  18. package/src/web-ui/styles.css +2868 -0
  19. package/src/web-ui.html +17 -0
  20. package/web-ui/app.js +273 -144
  21. package/web-ui/index.html +1310 -0
  22. package/web-ui/logic.mjs +21 -21
  23. package/web-ui/styles.css +2868 -0
  24. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  25. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  26. package/.github/workflows/ci.yml +0 -26
  27. package/.github/workflows/release.yml +0 -159
  28. package/.planning/.fix-attempts +0 -1
  29. package/.planning/.lock +0 -6
  30. package/.planning/.verify-cache.json +0 -14
  31. package/.planning/CHECKPOINT.json +0 -46
  32. package/.planning/DESIGN.md +0 -26
  33. package/.planning/HISTORY.json +0 -124
  34. package/.planning/PLAN.md +0 -69
  35. package/.planning/REVIEW.md +0 -41
  36. package/.planning/STATE.md +0 -12
  37. package/.planning/STATS.json +0 -13
  38. package/.planning/VERIFICATION.md +0 -70
  39. package/.planning/daude-code-plan.md +0 -51
  40. package/.planning/research/architecture.md +0 -32
  41. package/.planning/research/conventions.md +0 -36
  42. package/.planning/task_1-REVIEW.md +0 -29
  43. package/.planning/task_1-SUMMARY.md +0 -32
  44. package/.planning/task_2-REVIEW.md +0 -24
  45. package/.planning/task_2-SUMMARY.md +0 -37
  46. package/.planning/task_3-REVIEW.md +0 -25
  47. package/.planning/task_3-SUMMARY.md +0 -31
  48. package/cmd/publish-npm.cmd +0 -65
  49. package/tests/e2e/helpers.js +0 -214
  50. package/tests/e2e/recent-health.e2e.js +0 -142
  51. package/tests/e2e/run.js +0 -154
  52. package/tests/e2e/test-claude.js +0 -21
  53. package/tests/e2e/test-config.js +0 -124
  54. package/tests/e2e/test-health-speed.js +0 -79
  55. package/tests/e2e/test-openclaw.js +0 -47
  56. package/tests/e2e/test-session-search.js +0 -114
  57. package/tests/e2e/test-sessions.js +0 -69
  58. package/tests/e2e/test-setup.js +0 -159
  59. package/tests/unit/run.mjs +0 -29
  60. package/tests/unit/web-ui-logic.test.mjs +0 -186
  61. package/web-ui.html +0 -3977
  62. /package/{CHANGELOG.md → doc/CHANGELOG.md} +0 -0
  63. /package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +0 -0
@@ -1,114 +0,0 @@
1
- const http = require('http');
2
- const { assert } = require('./helpers');
3
-
4
- async function fetchHtml(port) {
5
- return new Promise((resolve, reject) => {
6
- const req = http.get({
7
- hostname: '127.0.0.1',
8
- port,
9
- path: '/',
10
- timeout: 2000
11
- }, (res) => {
12
- let body = '';
13
- res.setEncoding('utf-8');
14
- res.on('data', chunk => body += chunk);
15
- res.on('end', () => resolve(body));
16
- });
17
- req.on('error', reject);
18
- req.on('timeout', () => {
19
- req.destroy(new Error('timeout'));
20
- });
21
- });
22
- }
23
-
24
- module.exports = async function testSessionSearch(ctx) {
25
- const { api, sessionId, claudeSessionId, daudeSessionId } = ctx;
26
-
27
- const claudeSearch = await api('list-sessions', { source: 'claude', query: 'claudecode', limit: 20, forceRefresh: true });
28
- assert(Array.isArray(claudeSearch.sessions), 'claudecode query missing sessions');
29
- const claudeHit = claudeSearch.sessions.find(item => item.sessionId === claudeSessionId);
30
- assert(claudeHit, 'claudecode query missing Claude session');
31
- assert(claudeHit.provider === 'claude', 'Claude session provider missing');
32
- assert(claudeHit.capabilities && claudeHit.capabilities.code === true, 'Claude session code capability missing');
33
- assert(Array.isArray(claudeHit.keywords) && claudeHit.keywords.includes('claude_code'), 'Claude session keyword missing');
34
-
35
- const variantSearch = await api('list-sessions', { source: 'claude', query: 'claude-code', limit: 20, forceRefresh: true });
36
- assert(variantSearch.sessions.some(item => item.sessionId === claudeSessionId), 'claude-code query missing Claude session');
37
-
38
- const combined = await api('list-sessions', { source: 'claude', query: 'claude code hello', limit: 20, forceRefresh: true });
39
- const combinedIds = combined.sessions.map(item => item.sessionId);
40
- assert(combinedIds.includes(claudeSessionId), 'combined query missing Claude session');
41
-
42
- const baseline = await api('list-sessions', { source: 'codex', query: 'hello', limit: 20, forceRefresh: true });
43
- assert(baseline.sessions.some(item => item.sessionId === sessionId), 'baseline query missing codex session');
44
-
45
- const daudeSearch = await api('list-sessions', {
46
- source: 'codex',
47
- query: 'daude code',
48
- limit: 20,
49
- forceRefresh: true
50
- });
51
- assert(daudeSearch.sessions.some(item => item.sessionId === daudeSessionId), 'daude code query missing session');
52
-
53
- const daudeHyphen = await api('list-sessions', {
54
- source: 'codex',
55
- query: 'daude-code',
56
- limit: 20,
57
- forceRefresh: true
58
- });
59
- assert(daudeHyphen.sessions.some(item => item.sessionId === daudeSessionId), 'daude-code query missing session');
60
-
61
- const daudeConcat = await api('list-sessions', {
62
- source: 'codex',
63
- query: 'daudecode',
64
- limit: 20,
65
- forceRefresh: true
66
- });
67
- assert(daudeConcat.sessions.some(item => item.sessionId === daudeSessionId), 'daudecode query missing session');
68
-
69
- const highlighted = await api('list-sessions', {
70
- source: 'claude',
71
- query: 'claude code hello',
72
- queryScope: 'all',
73
- contentScanBytes: 8 * 1024,
74
- limit: 5,
75
- forceRefresh: true
76
- });
77
- const highlightedHit = highlighted.sessions.find(item => item.sessionId === claudeSessionId);
78
- assert(highlightedHit, 'highlight query missing Claude session');
79
- assert(highlightedHit.match && highlightedHit.match.hit === true, 'highlight match missing for Claude session');
80
- assert(Array.isArray(highlightedHit.match.snippets) && highlightedHit.match.snippets.some(
81
- snippet => typeof snippet === 'string' && snippet.toLowerCase().includes('hello from claude code session')
82
- ), 'highlight snippets missing Claude code text');
83
-
84
- const numeric = await api('list-sessions', {
85
- source: 'codex',
86
- query: '222',
87
- queryScope: 'all',
88
- contentScanBytes: 8 * 1024,
89
- limit: 10,
90
- forceRefresh: true
91
- });
92
- const numericHit = numeric.sessions.find(item => item.sessionId === daudeSessionId);
93
- assert(numericHit, '222 query missing daude session');
94
- assert(numericHit.match && numericHit.match.hit === true, '222 match missing daude session hit');
95
- assert(Array.isArray(numericHit.match.snippets) && numericHit.match.snippets.some(
96
- snippet => typeof snippet === 'string' && snippet.includes('222')
97
- ), '222 snippets missing numeric token');
98
-
99
- const paged = await api('list-sessions', {
100
- source: 'claude',
101
- query: 'claude code hello',
102
- queryScope: 'all',
103
- limit: 1,
104
- forceRefresh: true
105
- });
106
- assert(Array.isArray(paged.sessions) && paged.sessions.length === 1, 'paged search should return first page only');
107
- assert(paged.sessions[0].sessionId === claudeSessionId, 'paged search first item should be Claude session');
108
-
109
- const html = await fetchHtml(ctx.port);
110
- assert(html && html.includes('.session-item-snippet'), 'session snippet style missing');
111
- const lowerHtml = (html || '').toLowerCase();
112
- assert(lowerHtml.includes('white-space: nowrap'), 'snippet nowrap missing');
113
- assert(lowerHtml.includes('text-overflow: ellipsis'), 'snippet ellipsis missing');
114
- };
@@ -1,69 +0,0 @@
1
- const path = require('path');
2
- const { assert } = require('./helpers');
3
-
4
- module.exports = async function testSessions(ctx) {
5
- const { api, sessionId, tmpHome, claudeSessionId } = ctx;
6
-
7
- const apiSessions = await api('list-sessions', { source: 'codex', limit: 50, forceRefresh: true });
8
- assert(Array.isArray(apiSessions.sessions), 'api sessions missing');
9
- assert(apiSessions.sessions.some(item => item.sessionId === sessionId), 'api sessions missing codex entry');
10
-
11
- const apiSessionsClaude = await api('list-sessions', { source: 'claude', limit: 50, forceRefresh: true });
12
- assert(Array.isArray(apiSessionsClaude.sessions), 'api sessions(claude) missing');
13
- assert(apiSessionsClaude.sessions.some(item => item.sessionId === claudeSessionId), 'api sessions(claude) missing claude entry');
14
-
15
- const claudeCodeQuery = await api('list-sessions', { source: 'claude', query: 'claude code', limit: 50, forceRefresh: true });
16
- assert(Array.isArray(claudeCodeQuery.sessions), 'claude code query missing sessions');
17
- const claudeCodeHit = claudeCodeQuery.sessions.find(item => item.sessionId === claudeSessionId);
18
- assert(claudeCodeHit, 'claude code query missing Claude session');
19
- assert(claudeCodeHit.provider === 'claude', 'claude code query provider mismatch');
20
- assert(claudeCodeHit.capabilities && claudeCodeHit.capabilities.code === true, 'claude code query missing code capability');
21
- assert(Array.isArray(claudeCodeHit.keywords) && claudeCodeHit.keywords.includes('claude_code'), 'claude code query missing keyword');
22
-
23
- const sessionDetail = await api('session-detail', { source: 'codex', sessionId });
24
- assert(Array.isArray(sessionDetail.messages), 'session-detail missing messages');
25
-
26
- const sessionPlain = await api('session-plain', { source: 'codex', sessionId });
27
- assert(sessionPlain.text && sessionPlain.text.includes('world'), 'session-plain missing content');
28
-
29
- const sessionPlainMissing = await api('session-plain', { source: 'codex', sessionId: 'missing-session' });
30
- assert(sessionPlainMissing.error, 'session-plain should fail for missing session');
31
-
32
- const exportSession = await api('export-session', { source: 'codex', sessionId, maxMessages: 1 });
33
- assert(exportSession.content, 'export-session missing content');
34
- assert(exportSession.truncated === true, 'export-session should be truncated with maxMessages');
35
-
36
- const cloneResult = await api('clone-session', { source: 'codex', sessionId });
37
- assert(cloneResult.success === true, 'clone-session failed');
38
- assert(cloneResult.sessionId && cloneResult.sessionId !== sessionId, 'clone-session id invalid');
39
- assert(cloneResult.filePath && cloneResult.filePath.endsWith('.jsonl'), 'clone-session file path invalid');
40
- assert(cloneResult.filePath && require('fs').existsSync(cloneResult.filePath), 'clone-session file missing');
41
- const cloneSessionId = cloneResult.sessionId;
42
-
43
- const cloneInvalid = await api('clone-session', { source: 'claude', sessionId });
44
- assert(cloneInvalid.error, 'clone-session should reject non-codex source');
45
-
46
- const apiSessionsAfterClone = await api('list-sessions', { source: 'codex', limit: 50, forceRefresh: true });
47
- assert(Array.isArray(apiSessionsAfterClone.sessions), 'api sessions after clone missing');
48
- assert(
49
- apiSessionsAfterClone.sessions[0]
50
- && apiSessionsAfterClone.sessions[0].sessionId === cloneSessionId,
51
- 'clone session not latest'
52
- );
53
-
54
- const deleteResult = await api('delete-session', { source: 'codex', sessionId });
55
- assert(deleteResult.success === true, 'delete-session failed');
56
-
57
- const deleteMissing = await api('delete-session', { source: 'codex', sessionId });
58
- assert(deleteMissing.error, 'delete-session should fail for missing session');
59
-
60
- const detailMissing = await api('session-detail', { source: 'codex', sessionId });
61
- assert(detailMissing.error, 'session-detail should fail after delete');
62
-
63
- const apiSessionsAfterDelete = await api('list-sessions', { source: 'codex', limit: 50, forceRefresh: true });
64
- assert(!apiSessionsAfterDelete.sessions.some(item => item.sessionId === sessionId), 'deleted session still listed');
65
- assert(apiSessionsAfterDelete.sessions.some(item => item.sessionId === cloneSessionId), 'clone session missing after delete');
66
-
67
- Object.assign(ctx, { cloneSessionId });
68
- };
69
-
@@ -1,159 +0,0 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const {
4
- assert,
5
- runSync,
6
- runWithInput
7
- } = require('./helpers');
8
-
9
- module.exports = async function testSetup(ctx) {
10
- const { env, node, cliPath, mockProviderUrl, noModelsUrl, htmlModelsUrl, authFailUrl, tmpHome } = ctx;
11
-
12
- const setupInput = [
13
- '2',
14
- 'e2e',
15
- mockProviderUrl,
16
- 'sk-test',
17
- 'e2e-model',
18
- ''
19
- ].join('\n');
20
-
21
- const setupResult = await runWithInput(node, [cliPath, 'setup'], setupInput, { env });
22
- if (setupResult.status !== 0) {
23
- const errorText = setupResult.stderr || setupResult.stdout || '';
24
- if (errorText.includes('EPERM')) {
25
- ctx.skipE2E = 'child_process spawn blocked (EPERM) during setup';
26
- return;
27
- }
28
- assert(setupResult.status === 0, `setup failed: ${errorText}`);
29
- }
30
-
31
- const configPath = path.join(tmpHome, '.codex', 'config.toml');
32
- assert(fs.existsSync(configPath), 'config.toml missing');
33
- const configContent = fs.readFileSync(configPath, 'utf-8');
34
- assert(/model_provider\s*=\s*"e2e"/.test(configContent), 'model_provider not set');
35
- assert(/model\s*=\s*"e2e-model"/.test(configContent), 'model not set');
36
- assert(/\[model_providers\.e2e\]/.test(configContent), 'provider block missing');
37
- assert(configContent.includes(`base_url = "${mockProviderUrl}"`), 'base_url missing or mismatched');
38
-
39
- const authPath = path.join(tmpHome, '.codex', 'auth.json');
40
- const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
41
- assert(auth.OPENAI_API_KEY === 'sk-test', 'auth api_key mismatch');
42
-
43
- const modelsPath = path.join(tmpHome, '.codex', 'models.json');
44
- const models = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
45
- assert(models.includes('e2e-model'), 'custom model not added');
46
-
47
- const statusResult = runSync(node, [cliPath, 'status'], { env });
48
- assert(statusResult.status === 0, 'status failed');
49
- assert(statusResult.stdout.includes('e2e'), 'status provider not shown');
50
- assert(statusResult.stdout.includes('e2e-model'), 'status model not shown');
51
-
52
- const listResult = runSync(node, [cliPath, 'list'], { env });
53
- assert(listResult.status === 0, 'list failed');
54
- assert(listResult.stdout.includes('e2e'), 'list missing provider');
55
-
56
- const claudeModel = 'claude-e2e';
57
- const claudeResult = runSync(node, [cliPath, 'claude', mockProviderUrl, 'sk-claude', claudeModel], { env });
58
- assert(claudeResult.status === 0, `claude command failed: ${claudeResult.stderr || claudeResult.stdout}`);
59
- const claudeSettingsPath = path.join(tmpHome, '.claude', 'settings.json');
60
- assert(fs.existsSync(claudeSettingsPath), 'claude settings missing');
61
- const claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
62
- assert(claudeSettings.env && claudeSettings.env.ANTHROPIC_API_KEY === 'sk-claude', 'claude API key mismatch');
63
- assert(claudeSettings.env.ANTHROPIC_BASE_URL === mockProviderUrl, 'claude base url mismatch');
64
- assert(claudeSettings.env.ANTHROPIC_MODEL === claudeModel, 'claude model mismatch');
65
-
66
- const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
67
- fs.mkdirSync(sessionsDir, { recursive: true });
68
- const sessionId = 'e2e-session';
69
- const sessionPath = path.join(sessionsDir, `${sessionId}.jsonl`);
70
- const sessionRecords = [
71
- {
72
- type: 'session_meta',
73
- payload: { id: sessionId, cwd: '/tmp/e2e' },
74
- timestamp: '2025-01-01T00:00:00.000Z'
75
- },
76
- {
77
- type: 'response_item',
78
- payload: { type: 'message', role: 'user', content: 'hello' },
79
- timestamp: '2025-01-01T00:00:01.000Z'
80
- },
81
- {
82
- type: 'response_item',
83
- payload: { type: 'message', role: 'assistant', content: 'world' },
84
- timestamp: '2025-01-01T00:00:02.000Z'
85
- }
86
- ];
87
- fs.writeFileSync(sessionPath, sessionRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
88
-
89
- const daudeSessionId = 'daude-e2e-session';
90
- const daudeSessionPath = path.join(sessionsDir, `${daudeSessionId}.jsonl`);
91
- const daudeRecords = [
92
- {
93
- type: 'session_meta',
94
- payload: { id: daudeSessionId, cwd: '/tmp/daude' },
95
- timestamp: '2025-02-02T00:00:00.000Z'
96
- },
97
- {
98
- type: 'response_item',
99
- payload: { type: 'message', role: 'user', content: 'daude code quick start 222' },
100
- timestamp: '2025-02-02T00:00:01.000Z'
101
- },
102
- {
103
- type: 'response_item',
104
- payload: { type: 'message', role: 'assistant', content: 'sharing daude-code bootstrap' },
105
- timestamp: '2025-02-02T00:00:02.000Z'
106
- }
107
- ];
108
- fs.writeFileSync(daudeSessionPath, daudeRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
109
-
110
- const claudeProjectsDir = path.join(tmpHome, '.claude', 'projects');
111
- const claudeProjectDir = path.join(claudeProjectsDir, 'e2e-project');
112
- fs.mkdirSync(claudeProjectDir, { recursive: true });
113
- const claudeSessionId = 'claude-e2e-session';
114
- const claudeSessionPath = path.join(claudeProjectDir, `${claudeSessionId}.jsonl`);
115
- const claudeRecords = [
116
- {
117
- type: 'user',
118
- message: { content: 'hello from claude code session' },
119
- timestamp: '2025-02-01T00:00:00.000Z'
120
- },
121
- {
122
- type: 'assistant',
123
- message: { content: 'initialized project' },
124
- timestamp: '2025-02-01T00:00:01.000Z'
125
- }
126
- ];
127
- fs.writeFileSync(claudeSessionPath, claudeRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
128
- const claudeIndexPath = path.join(claudeProjectDir, 'sessions-index.json');
129
- const claudeIndex = {
130
- entries: [
131
- {
132
- sessionId: claudeSessionId,
133
- projectPath: claudeProjectDir,
134
- fullPath: claudeSessionPath,
135
- created: '2025-02-01T00:00:00.000Z',
136
- modified: '2025-02-01T00:00:01.000Z',
137
- summary: 'Claude Code sample session',
138
- provider: 'claude',
139
- capabilities: { code: true },
140
- keywords: ['claude_code', 'sample'],
141
- messageCount: 2
142
- }
143
- ]
144
- };
145
- fs.writeFileSync(claudeIndexPath, JSON.stringify(claudeIndex, null, 2), 'utf-8');
146
-
147
- Object.assign(ctx, {
148
- claudeModel,
149
- sessionId,
150
- sessionPath,
151
- daudeSessionId,
152
- daudeSessionPath,
153
- claudeSessionId,
154
- claudeSessionPath,
155
- noModelsUrl,
156
- htmlModelsUrl,
157
- authFailUrl
158
- });
159
- };
@@ -1,29 +0,0 @@
1
- import path from 'path';
2
- import { fileURLToPath, pathToFileURL } from 'url';
3
-
4
- const __filename = fileURLToPath(import.meta.url);
5
- const __dirname = path.dirname(__filename);
6
-
7
- const tests = [];
8
- globalThis.test = (name, fn) => tests.push({ name, fn });
9
-
10
- await import(pathToFileURL(path.join(__dirname, 'web-ui-logic.test.mjs')));
11
-
12
- let failures = 0;
13
- for (const { name, fn } of tests) {
14
- try {
15
- await fn();
16
- console.log(`\u2713 ${name}`);
17
- } catch (err) {
18
- failures += 1;
19
- console.error(`\u2717 ${name}`);
20
- console.error(err);
21
- }
22
- }
23
-
24
- if (failures) {
25
- console.error(`Failed ${failures} test(s).`);
26
- process.exit(1);
27
- } else {
28
- console.log(`All ${tests.length} tests passed.`);
29
- }
@@ -1,186 +0,0 @@
1
- import assert from 'assert';
2
- import path from 'path';
3
- import { fileURLToPath, pathToFileURL } from 'url';
4
-
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
7
-
8
- const logic = await import(pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'logic.mjs')));
9
- const {
10
- normalizeClaudeValue,
11
- normalizeClaudeConfig,
12
- normalizeClaudeSettingsEnv,
13
- matchClaudeConfigFromSettings,
14
- findDuplicateClaudeConfigName,
15
- formatLatency,
16
- buildSpeedTestIssue,
17
- isSessionQueryEnabled,
18
- buildSessionListParams
19
- } = logic;
20
-
21
- test('normalizeClaudeValue trims strings and ignores non-string', () => {
22
- assert.strictEqual(normalizeClaudeValue(' abc '), 'abc');
23
- assert.strictEqual(normalizeClaudeValue(123), '');
24
- assert.strictEqual(normalizeClaudeValue(null), '');
25
- });
26
-
27
- test('normalizeClaudeConfig trims all fields', () => {
28
- const cfg = normalizeClaudeConfig({ apiKey: ' key ', baseUrl: ' url ', model: ' model ' });
29
- assert.deepStrictEqual(cfg, { apiKey: 'key', baseUrl: 'url', model: 'model' });
30
- });
31
-
32
- test('normalizeClaudeSettingsEnv trims settings env', () => {
33
- const env = { ANTHROPIC_API_KEY: ' key ', ANTHROPIC_BASE_URL: ' url ', ANTHROPIC_MODEL: ' model ' };
34
- assert.deepStrictEqual(normalizeClaudeSettingsEnv(env), { apiKey: 'key', baseUrl: 'url', model: 'model' });
35
- });
36
-
37
- test('normalizeClaudeSettingsEnv fills missing fields with empty strings', () => {
38
- const env = { ANTHROPIC_API_KEY: 'k' };
39
- assert.deepStrictEqual(normalizeClaudeSettingsEnv(env), { apiKey: 'k', baseUrl: '', model: '' });
40
- });
41
-
42
- test('matchClaudeConfigFromSettings matches identical config', () => {
43
- const configs = { default: { apiKey: 'k', baseUrl: 'u', model: 'm' } };
44
- const env = { ANTHROPIC_API_KEY: 'k', ANTHROPIC_BASE_URL: 'u', ANTHROPIC_MODEL: 'm' };
45
- assert.strictEqual(matchClaudeConfigFromSettings(configs, env), 'default');
46
- });
47
-
48
- test('matchClaudeConfigFromSettings returns empty when incomplete', () => {
49
- const configs = { default: { apiKey: 'k', baseUrl: 'u', model: 'm' } };
50
- const env = { ANTHROPIC_API_KEY: 'k', ANTHROPIC_BASE_URL: '', ANTHROPIC_MODEL: 'm' };
51
- assert.strictEqual(matchClaudeConfigFromSettings(configs, env), '');
52
- });
53
-
54
- test('findDuplicateClaudeConfigName returns empty on missing fields', () => {
55
- const configs = { only: { apiKey: 'k', baseUrl: 'u', model: 'm' } };
56
- const incomplete = { apiKey: 'k', baseUrl: '', model: '' };
57
- assert.strictEqual(findDuplicateClaudeConfigName(configs, incomplete), '');
58
- });
59
-
60
- test('findDuplicateClaudeConfigName detects duplicates', () => {
61
- const configs = {
62
- first: { apiKey: 'k1', baseUrl: 'u1', model: 'm1' },
63
- second: { apiKey: 'k2', baseUrl: 'u2', model: 'm2' }
64
- };
65
- const duplicate = { apiKey: 'k2', baseUrl: 'u2', model: 'm2' };
66
- assert.strictEqual(findDuplicateClaudeConfigName(configs, duplicate), 'second');
67
- });
68
-
69
- test('findDuplicateClaudeConfigName returns empty when no match', () => {
70
- const configs = { only: { apiKey: 'k', baseUrl: 'u', model: 'm' } };
71
- const another = { apiKey: 'k', baseUrl: 'u', model: 'm-2' };
72
- assert.strictEqual(findDuplicateClaudeConfigName(configs, another), '');
73
- });
74
-
75
- test('formatLatency formats success and errors', () => {
76
- assert.strictEqual(formatLatency({ ok: true, durationMs: 120 }), '120ms');
77
- assert.strictEqual(formatLatency({ ok: false, status: 404 }), 'ERR 404');
78
- assert.strictEqual(formatLatency({ ok: false }), 'ERR');
79
- assert.strictEqual(formatLatency(null), '');
80
- assert.strictEqual(formatLatency({ ok: true, durationMs: undefined }), '0ms');
81
- assert.strictEqual(formatLatency({ ok: true, durationMs: '12' }), '0ms');
82
- });
83
-
84
- test('buildSpeedTestIssue maps errors and status codes', () => {
85
- assert.strictEqual(buildSpeedTestIssue('p1', null), null);
86
- const missing = buildSpeedTestIssue('p1', { error: 'Provider not found' });
87
- assert.strictEqual(missing.code, 'remote-speedtest-provider-missing');
88
-
89
- const timeout = buildSpeedTestIssue('p1', { error: 'Request timeout' });
90
- assert.strictEqual(timeout.code, 'remote-speedtest-timeout');
91
-
92
- const auth = buildSpeedTestIssue('p1', { ok: false, status: 401 });
93
- assert.strictEqual(auth.code, 'remote-speedtest-auth-failed');
94
-
95
- const httpErr = buildSpeedTestIssue('p1', { ok: false, status: 500 });
96
- assert.strictEqual(httpErr.code, 'remote-speedtest-http-error');
97
-
98
- const ok = buildSpeedTestIssue('p1', { ok: true, status: 200 });
99
- assert.strictEqual(ok, null);
100
-
101
- const invalidUrl = buildSpeedTestIssue('p1', { error: 'Invalid URL' });
102
- assert.strictEqual(invalidUrl.code, 'remote-speedtest-invalid-url');
103
-
104
- const missingUrl = buildSpeedTestIssue('p1', { error: 'Missing name or url' });
105
- assert.strictEqual(missingUrl.code, 'remote-speedtest-baseurl-missing');
106
-
107
- const timeoutLower = buildSpeedTestIssue('p1', { error: 'timeout while fetching' });
108
- assert.strictEqual(timeoutLower.code, 'remote-speedtest-timeout');
109
-
110
- const generic = buildSpeedTestIssue('p1', { error: 'network unreachable' });
111
- assert.strictEqual(generic.code, 'remote-speedtest-unreachable');
112
-
113
- const auth403 = buildSpeedTestIssue('p1', { ok: false, status: 403 });
114
- assert.strictEqual(auth403.code, 'remote-speedtest-auth-failed');
115
-
116
- const http400 = buildSpeedTestIssue('p1', { ok: false, status: 400 });
117
- assert.strictEqual(http400.code, 'remote-speedtest-http-error');
118
- });
119
-
120
- test('isSessionQueryEnabled supports codex and claude only', () => {
121
- assert.strictEqual(isSessionQueryEnabled('codex'), true);
122
- assert.strictEqual(isSessionQueryEnabled('CODEX'), true);
123
- assert.strictEqual(isSessionQueryEnabled('claude'), true);
124
- assert.strictEqual(isSessionQueryEnabled('ALL'), false);
125
- assert.strictEqual(isSessionQueryEnabled('openai'), false);
126
- assert.strictEqual(isSessionQueryEnabled(''), false);
127
- });
128
-
129
- test('buildSessionListParams keeps claude code lexicon query when enabled', () => {
130
- const paramsClaude = buildSessionListParams({
131
- source: 'claude',
132
- query: 'claude code',
133
- roleFilter: 'all'
134
- });
135
- assert.strictEqual(paramsClaude.query, 'claude code');
136
- assert.strictEqual(paramsClaude.queryMode, 'and');
137
- assert.strictEqual(paramsClaude.queryScope, 'content');
138
- });
139
-
140
- test('buildSessionListParams keeps query for enabled sources', () => {
141
- const paramsCodex = buildSessionListParams({
142
- source: 'codex',
143
- query: 'test',
144
- pathFilter: ''
145
- });
146
- assert.strictEqual(paramsCodex.query, 'test');
147
- assert.strictEqual(paramsCodex.source, 'codex');
148
- assert.strictEqual(paramsCodex.limit, 200);
149
-
150
- const paramsClaude = buildSessionListParams({
151
- source: 'claude',
152
- query: 'claude code',
153
- roleFilter: 'user'
154
- });
155
- assert.strictEqual(paramsClaude.query, 'claude code');
156
- assert.strictEqual(paramsClaude.source, 'claude');
157
- assert.strictEqual(paramsClaude.roleFilter, 'user');
158
- assert.strictEqual(paramsClaude.limit, 200);
159
-
160
- const paramsAll = buildSessionListParams({
161
- source: 'codex',
162
- query: 'claudecode',
163
- timeRangePreset: '7d'
164
- });
165
- assert.strictEqual(paramsAll.query, 'claudecode');
166
- assert.strictEqual(paramsAll.source, 'codex');
167
- assert.strictEqual(paramsAll.timeRangePreset, '7d');
168
- assert.strictEqual(paramsAll.limit, 200);
169
- });
170
-
171
- test('buildSessionListParams clears query for unsupported sources', () => {
172
- const params = buildSessionListParams({
173
- source: 'openai',
174
- query: 'hello',
175
- pathFilter: '/tmp',
176
- roleFilter: 'assistant'
177
- });
178
- assert.strictEqual(params.query, '');
179
- assert.strictEqual(params.source, 'openai');
180
- assert.strictEqual(params.pathFilter, '/tmp');
181
- assert.strictEqual(params.roleFilter, 'assistant');
182
- assert.strictEqual(params.limit, 200);
183
- assert.strictEqual(params.forceRefresh, true);
184
- assert.strictEqual(params.queryScope, 'content');
185
- assert.strictEqual(params.contentScanLimit, 50);
186
- });