codexmate 0.0.8 → 0.0.9
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.
- package/.planning/.fix-attempts +1 -0
- package/.planning/.lock +6 -0
- package/.planning/.verify-cache.json +14 -0
- package/.planning/CHECKPOINT.json +46 -0
- package/.planning/DESIGN.md +26 -0
- package/.planning/HISTORY.json +124 -0
- package/.planning/PLAN.md +69 -0
- package/.planning/REVIEW.md +41 -0
- package/.planning/STATE.md +12 -0
- package/.planning/STATS.json +13 -0
- package/.planning/VERIFICATION.md +70 -0
- package/.planning/daude-code-plan.md +51 -0
- package/.planning/research/architecture.md +32 -0
- package/.planning/research/conventions.md +36 -0
- package/.planning/task_1-REVIEW.md +29 -0
- package/.planning/task_1-SUMMARY.md +32 -0
- package/.planning/task_2-REVIEW.md +24 -0
- package/.planning/task_2-SUMMARY.md +37 -0
- package/.planning/task_3-REVIEW.md +25 -0
- package/.planning/task_3-SUMMARY.md +31 -0
- package/README.md +10 -12
- package/README.zh-CN.md +20 -16
- package/cli.js +291 -156
- package/lib/cli-file-utils.js +9 -7
- package/package.json +3 -2
- package/res/json5.min.js +1 -0
- package/res/vue.global.js +18552 -0
- package/tests/e2e/run.js +19 -4
- package/tests/e2e/test-health-speed.js +5 -1
- package/tests/e2e/test-session-search.js +114 -0
- package/tests/e2e/test-sessions.js +22 -13
- package/tests/e2e/test-setup.js +83 -14
- package/tests/unit/run.mjs +29 -0
- package/tests/unit/web-ui-logic.test.mjs +186 -0
- package/web-ui/app.js +2841 -0
- package/web-ui/logic.mjs +157 -0
- package/web-ui.html +574 -3014
package/tests/e2e/run.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
const testSetup = require('./test-setup');
|
|
16
16
|
const testConfig = require('./test-config');
|
|
17
17
|
const testClaude = require('./test-claude');
|
|
18
|
+
const testSessionSearch = require('./test-session-search');
|
|
18
19
|
const testSessions = require('./test-sessions');
|
|
19
20
|
const testOpenclaw = require('./test-openclaw');
|
|
20
21
|
const testHealthSpeed = require('./test-health-speed');
|
|
@@ -68,13 +69,26 @@ async function main() {
|
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
await testSetup(ctx);
|
|
72
|
+
if (ctx.skipE2E) {
|
|
73
|
+
console.warn(`E2E skipped: ${ctx.skipE2E}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
71
76
|
|
|
72
77
|
const port = 18000 + Math.floor(Math.random() * 1000);
|
|
73
78
|
debug('start web server');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
try {
|
|
80
|
+
webServer = spawn(node, [cliPath, 'run'], {
|
|
81
|
+
env: { ...env, CODEXMATE_PORT: String(port) },
|
|
82
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err && err.code === 'EPERM') {
|
|
86
|
+
console.warn('E2E skipped: child_process spawn blocked (EPERM) when starting server');
|
|
87
|
+
ctx.skipE2E = ctx.skipE2E || 'child_process spawn blocked (EPERM) when starting server';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
78
92
|
webServer.stdout.on('data', () => {});
|
|
79
93
|
webServer.stderr.on('data', () => {});
|
|
80
94
|
|
|
@@ -86,6 +100,7 @@ async function main() {
|
|
|
86
100
|
|
|
87
101
|
await testConfig(ctx);
|
|
88
102
|
await testClaude(ctx);
|
|
103
|
+
await testSessionSearch(ctx);
|
|
89
104
|
await testSessions(ctx);
|
|
90
105
|
await testOpenclaw(ctx);
|
|
91
106
|
await testHealthSpeed(ctx);
|
|
@@ -71,5 +71,9 @@ module.exports = async function testHealthAndSpeed(ctx) {
|
|
|
71
71
|
|
|
72
72
|
const newPath = path.join(permDir, 'new.json');
|
|
73
73
|
writeJsonAtomic(newPath, { b: 1 });
|
|
74
|
-
|
|
74
|
+
const expectedNewMode = process.platform === 'win32' ? 0o666 : 0o600;
|
|
75
|
+
assert(
|
|
76
|
+
fileMode(newPath) === expectedNewMode,
|
|
77
|
+
`writeJsonAtomic should default to ${expectedNewMode.toString(8)} for new file (got ${fileMode(newPath).toString(8)})`
|
|
78
|
+
);
|
|
75
79
|
};
|
|
@@ -0,0 +1,114 @@
|
|
|
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,19 +1,27 @@
|
|
|
1
|
-
const path = require('path');
|
|
1
|
+
const path = require('path');
|
|
2
2
|
const { assert } = require('./helpers');
|
|
3
3
|
|
|
4
4
|
module.exports = async function testSessions(ctx) {
|
|
5
|
-
const { api, sessionId, tmpHome } = 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
|
|
12
|
-
assert(Array.isArray(
|
|
13
|
-
assert(
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
assert(Array.isArray(
|
|
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');
|
|
17
25
|
|
|
18
26
|
const sessionPlain = await api('session-plain', { source: 'codex', sessionId });
|
|
19
27
|
assert(sessionPlain.text && sessionPlain.text.includes('world'), 'session-plain missing content');
|
|
@@ -58,3 +66,4 @@ module.exports = async function testSessions(ctx) {
|
|
|
58
66
|
|
|
59
67
|
Object.assign(ctx, { cloneSessionId });
|
|
60
68
|
};
|
|
69
|
+
|
package/tests/e2e/test-setup.js
CHANGED
|
@@ -19,7 +19,14 @@ module.exports = async function testSetup(ctx) {
|
|
|
19
19
|
].join('\n');
|
|
20
20
|
|
|
21
21
|
const setupResult = await runWithInput(node, [cliPath, 'setup'], setupInput, { env });
|
|
22
|
-
|
|
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
|
+
}
|
|
23
30
|
|
|
24
31
|
const configPath = path.join(tmpHome, '.codex', 'config.toml');
|
|
25
32
|
assert(fs.existsSync(configPath), 'config.toml missing');
|
|
@@ -56,11 +63,11 @@ module.exports = async function testSetup(ctx) {
|
|
|
56
63
|
assert(claudeSettings.env.ANTHROPIC_BASE_URL === mockProviderUrl, 'claude base url mismatch');
|
|
57
64
|
assert(claudeSettings.env.ANTHROPIC_MODEL === claudeModel, 'claude model mismatch');
|
|
58
65
|
|
|
59
|
-
const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
|
|
60
|
-
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
61
|
-
const sessionId = 'e2e-session';
|
|
62
|
-
const sessionPath = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
63
|
-
const sessionRecords = [
|
|
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 = [
|
|
64
71
|
{
|
|
65
72
|
type: 'session_meta',
|
|
66
73
|
payload: { id: sessionId, cwd: '/tmp/e2e' },
|
|
@@ -76,15 +83,77 @@ module.exports = async function testSetup(ctx) {
|
|
|
76
83
|
payload: { type: 'message', role: 'assistant', content: 'world' },
|
|
77
84
|
timestamp: '2025-01-01T00:00:02.000Z'
|
|
78
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
|
+
}
|
|
79
126
|
];
|
|
80
|
-
fs.writeFileSync(
|
|
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');
|
|
81
146
|
|
|
82
|
-
Object.assign(ctx, {
|
|
83
|
-
claudeModel,
|
|
84
|
-
sessionId,
|
|
85
|
-
sessionPath,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
147
|
+
Object.assign(ctx, {
|
|
148
|
+
claudeModel,
|
|
149
|
+
sessionId,
|
|
150
|
+
sessionPath,
|
|
151
|
+
daudeSessionId,
|
|
152
|
+
daudeSessionPath,
|
|
153
|
+
claudeSessionId,
|
|
154
|
+
claudeSessionPath,
|
|
155
|
+
noModelsUrl,
|
|
156
|
+
htmlModelsUrl,
|
|
157
|
+
authFailUrl
|
|
89
158
|
});
|
|
90
159
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
});
|