codexmate 0.0.7 → 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/.github/workflows/release.yml +122 -8
- 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 +58 -52
- package/README.zh-CN.md +68 -56
- package/cli.js +1142 -1427
- package/lib/cli-file-utils.js +151 -0
- package/lib/cli-models-utils.js +152 -0
- package/lib/cli-network-utils.js +148 -0
- package/lib/cli-session-utils.js +121 -0
- package/lib/cli-utils.js +139 -0
- package/package.json +4 -2
- package/res/json5.min.js +1 -0
- package/res/vue.global.js +18552 -0
- package/tests/e2e/helpers.js +214 -0
- package/tests/e2e/recent-health.e2e.js +6 -0
- package/tests/e2e/run.js +103 -306
- package/tests/e2e/test-claude.js +21 -0
- package/tests/e2e/test-config.js +124 -0
- package/tests/e2e/test-health-speed.js +79 -0
- package/tests/e2e/test-openclaw.js +47 -0
- package/tests/e2e/test-session-search.js +114 -0
- package/tests/e2e/test-sessions.js +69 -0
- package/tests/e2e/test-setup.js +159 -0
- 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 +1045 -2996
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { spawnSync, spawn } = require('child_process');
|
|
6
|
+
const { writeJsonAtomic } = require('../../lib/cli-file-utils');
|
|
7
|
+
const { normalizeWireApi, buildModelProbeSpec } = require('../../lib/cli-models-utils');
|
|
8
|
+
|
|
9
|
+
const debug = (...args) => {
|
|
10
|
+
if (process.env.E2E_DEBUG) {
|
|
11
|
+
console.error('[e2e]', ...args);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function assert(condition, message) {
|
|
16
|
+
if (!condition) {
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fileMode(filePath) {
|
|
22
|
+
return fs.existsSync(filePath) ? (fs.statSync(filePath).mode & 0o777) : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function captureFileState(filePath) {
|
|
26
|
+
const state = {
|
|
27
|
+
path: filePath,
|
|
28
|
+
exists: false,
|
|
29
|
+
readable: true,
|
|
30
|
+
content: '',
|
|
31
|
+
error: ''
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
state.exists = fs.existsSync(filePath);
|
|
35
|
+
if (!state.exists) {
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
state.content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
} catch (e) {
|
|
42
|
+
state.readable = false;
|
|
43
|
+
state.error = e && e.message ? e.message : String(e);
|
|
44
|
+
}
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertFileUnchanged(state, label) {
|
|
49
|
+
if (!state || !state.readable) return;
|
|
50
|
+
const name = label || state.path;
|
|
51
|
+
if (state.exists) {
|
|
52
|
+
assert(fs.existsSync(state.path), `${name} disappeared during e2e`);
|
|
53
|
+
const current = fs.readFileSync(state.path, 'utf-8');
|
|
54
|
+
assert(current === state.content, `${name} changed during e2e`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
assert(!fs.existsSync(state.path), `${name} should not be created during e2e`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runSync(node, args, options = {}) {
|
|
61
|
+
const result = spawnSync(node, args, {
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
...options
|
|
64
|
+
});
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runWithInput(node, args, input, options = {}) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let child;
|
|
71
|
+
try {
|
|
72
|
+
child = spawn(node, args, { ...options, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return resolve({
|
|
75
|
+
status: 1,
|
|
76
|
+
stdout: '',
|
|
77
|
+
stderr: err && err.message ? err.message : String(err)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
let stdout = '';
|
|
81
|
+
let stderr = '';
|
|
82
|
+
child.stdout.on('data', chunk => stdout += chunk.toString());
|
|
83
|
+
child.stderr.on('data', chunk => stderr += chunk.toString());
|
|
84
|
+
child.on('error', (err) => {
|
|
85
|
+
resolve({
|
|
86
|
+
status: 1,
|
|
87
|
+
stdout,
|
|
88
|
+
stderr: stderr || (err && err.message ? err.message : String(err))
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
child.on('close', (code) => resolve({ status: code, stdout, stderr }));
|
|
92
|
+
if (input) {
|
|
93
|
+
child.stdin.write(input);
|
|
94
|
+
}
|
|
95
|
+
child.stdin.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function postJson(port, payload, timeoutMs = 2000) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const data = JSON.stringify(payload);
|
|
102
|
+
const req = http.request({
|
|
103
|
+
hostname: '127.0.0.1',
|
|
104
|
+
port,
|
|
105
|
+
path: '/api',
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
'Content-Length': Buffer.byteLength(data)
|
|
110
|
+
}
|
|
111
|
+
}, (res) => {
|
|
112
|
+
let body = '';
|
|
113
|
+
res.setEncoding('utf-8');
|
|
114
|
+
res.on('data', chunk => body += chunk);
|
|
115
|
+
res.on('end', () => {
|
|
116
|
+
try {
|
|
117
|
+
resolve(JSON.parse(body || '{}'));
|
|
118
|
+
} catch (e) {
|
|
119
|
+
reject(new Error('Invalid JSON response'));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
req.on('error', reject);
|
|
125
|
+
req.setTimeout(timeoutMs, () => {
|
|
126
|
+
req.destroy(new Error('Request timeout'));
|
|
127
|
+
});
|
|
128
|
+
req.write(data);
|
|
129
|
+
req.end();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function waitForServer(port, retries = 20, delayMs = 200) {
|
|
134
|
+
let lastError;
|
|
135
|
+
for (let i = 0; i < retries; i++) {
|
|
136
|
+
try {
|
|
137
|
+
await postJson(port, { action: 'status' }, 1000);
|
|
138
|
+
return;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
lastError = e;
|
|
141
|
+
debug(`wait retry ${i + 1}/${retries}: ${e && e.message ? e.message : e}`);
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw lastError || new Error('Server not ready');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function startLocalServer(options = {}) {
|
|
149
|
+
const mode = options.mode || 'list';
|
|
150
|
+
const modelsPath = options.modelsPath || '/models';
|
|
151
|
+
const status = options.status || 200;
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const server = http.createServer((req, res) => {
|
|
154
|
+
if (req.url && req.url.startsWith(modelsPath)) {
|
|
155
|
+
if (mode === 'none') {
|
|
156
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
157
|
+
res.end(JSON.stringify({ error: 'not found' }));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (mode === 'html') {
|
|
161
|
+
res.writeHead(status, { 'Content-Type': 'text/html' });
|
|
162
|
+
res.end('<!doctype html><html><body>ok</body></html>');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
166
|
+
res.end(JSON.stringify({
|
|
167
|
+
data: [
|
|
168
|
+
{ id: 'e2e2-model' },
|
|
169
|
+
{ id: 'e2e2-model-2' }
|
|
170
|
+
]
|
|
171
|
+
}));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
175
|
+
res.end(JSON.stringify({ ok: true }));
|
|
176
|
+
});
|
|
177
|
+
server.on('error', reject);
|
|
178
|
+
server.listen(0, '127.0.0.1', () => {
|
|
179
|
+
const address = server.address();
|
|
180
|
+
resolve({ server, port: address.port });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function closeServer(server) {
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
if (!server) return resolve();
|
|
188
|
+
try {
|
|
189
|
+
server.close(() => resolve());
|
|
190
|
+
} catch (e) {
|
|
191
|
+
resolve();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
fs,
|
|
198
|
+
path,
|
|
199
|
+
os,
|
|
200
|
+
debug,
|
|
201
|
+
assert,
|
|
202
|
+
fileMode,
|
|
203
|
+
captureFileState,
|
|
204
|
+
assertFileUnchanged,
|
|
205
|
+
runSync,
|
|
206
|
+
runWithInput,
|
|
207
|
+
postJson,
|
|
208
|
+
waitForServer,
|
|
209
|
+
startLocalServer,
|
|
210
|
+
closeServer,
|
|
211
|
+
writeJsonAtomic,
|
|
212
|
+
normalizeWireApi,
|
|
213
|
+
buildModelProbeSpec
|
|
214
|
+
};
|
|
@@ -96,6 +96,12 @@ async function run() {
|
|
|
96
96
|
await request('apply-config-template', { template: buildTemplate('gamma', 'm-gamma', 'https://example.com', 'sk-gamma') });
|
|
97
97
|
await request('apply-config-template', { template: buildTemplate('delta', 'm-delta', 'https://example.com', 'sk-delta') });
|
|
98
98
|
|
|
99
|
+
const share = await request('export-provider', { name: 'delta' });
|
|
100
|
+
assert(share && share.payload, 'share payload should exist');
|
|
101
|
+
assert(share.payload.name === 'delta', 'share name should be delta');
|
|
102
|
+
assert(share.payload.baseUrl === 'https://example.com', 'share baseUrl mismatch');
|
|
103
|
+
assert(share.payload.apiKey === 'sk-delta', 'share apiKey mismatch');
|
|
104
|
+
|
|
99
105
|
const recent = await request('get-recent-configs');
|
|
100
106
|
assert(Array.isArray(recent.items), 'recent list should be array');
|
|
101
107
|
assert(recent.items.length === 3, 'recent list should keep 3 items');
|
package/tests/e2e/run.js
CHANGED
|
@@ -1,133 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
const os = require('os');
|
|
1
|
+
const { spawn } = require('child_process');
|
|
3
2
|
const path = require('path');
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const child = spawn(node, args, { ...options, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
24
|
-
let stdout = '';
|
|
25
|
-
let stderr = '';
|
|
26
|
-
child.stdout.on('data', chunk => stdout += chunk.toString());
|
|
27
|
-
child.stderr.on('data', chunk => stderr += chunk.toString());
|
|
28
|
-
child.on('close', (code) => resolve({ status: code, stdout, stderr }));
|
|
29
|
-
if (input) {
|
|
30
|
-
child.stdin.write(input);
|
|
31
|
-
}
|
|
32
|
-
child.stdin.end();
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
function postJson(port, payload, timeoutMs = 2000) {
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
const data = JSON.stringify(payload);
|
|
38
|
-
const req = http.request({
|
|
39
|
-
hostname: '127.0.0.1',
|
|
40
|
-
port,
|
|
41
|
-
path: '/api',
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
'Content-Length': Buffer.byteLength(data)
|
|
46
|
-
}
|
|
47
|
-
}, (res) => {
|
|
48
|
-
let body = '';
|
|
49
|
-
res.setEncoding('utf-8');
|
|
50
|
-
res.on('data', chunk => body += chunk);
|
|
51
|
-
res.on('end', () => {
|
|
52
|
-
try {
|
|
53
|
-
resolve(JSON.parse(body || '{}'));
|
|
54
|
-
} catch (e) {
|
|
55
|
-
reject(new Error('Invalid JSON response'));
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
req.on('error', reject);
|
|
61
|
-
req.setTimeout(timeoutMs, () => {
|
|
62
|
-
req.destroy(new Error('Request timeout'));
|
|
63
|
-
});
|
|
64
|
-
req.write(data);
|
|
65
|
-
req.end();
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function waitForServer(port, retries = 20, delayMs = 200) {
|
|
70
|
-
let lastError;
|
|
71
|
-
for (let i = 0; i < retries; i++) {
|
|
72
|
-
try {
|
|
73
|
-
await postJson(port, { action: 'status' }, 1000);
|
|
74
|
-
return;
|
|
75
|
-
} catch (e) {
|
|
76
|
-
lastError = e;
|
|
77
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
throw lastError || new Error('Server not ready');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function startLocalServer(options = {}) {
|
|
84
|
-
const mode = options.mode || 'list';
|
|
85
|
-
const modelsPath = options.modelsPath || '/models';
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
const server = http.createServer((req, res) => {
|
|
88
|
-
if (req.url && req.url.startsWith(modelsPath)) {
|
|
89
|
-
if (mode === 'none') {
|
|
90
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
91
|
-
res.end(JSON.stringify({ error: 'not found' }));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (mode === 'html') {
|
|
95
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
96
|
-
res.end('<!doctype html><html><body>ok</body></html>');
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
100
|
-
res.end(JSON.stringify({
|
|
101
|
-
data: [
|
|
102
|
-
{ id: 'e2e2-model' },
|
|
103
|
-
{ id: 'e2e2-model-2' }
|
|
104
|
-
]
|
|
105
|
-
}));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
109
|
-
res.end(JSON.stringify({ ok: true }));
|
|
110
|
-
});
|
|
111
|
-
server.on('error', reject);
|
|
112
|
-
server.listen(0, '127.0.0.1', () => {
|
|
113
|
-
const address = server.address();
|
|
114
|
-
resolve({ server, port: address.port });
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function closeServer(server) {
|
|
120
|
-
return new Promise((resolve) => {
|
|
121
|
-
if (!server) return resolve();
|
|
122
|
-
try {
|
|
123
|
-
server.close(() => resolve());
|
|
124
|
-
} catch (e) {
|
|
125
|
-
resolve();
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const {
|
|
5
|
+
os,
|
|
6
|
+
debug,
|
|
7
|
+
captureFileState,
|
|
8
|
+
assertFileUnchanged,
|
|
9
|
+
startLocalServer,
|
|
10
|
+
closeServer,
|
|
11
|
+
waitForServer,
|
|
12
|
+
postJson
|
|
13
|
+
} = require('./helpers');
|
|
14
|
+
|
|
15
|
+
const testSetup = require('./test-setup');
|
|
16
|
+
const testConfig = require('./test-config');
|
|
17
|
+
const testClaude = require('./test-claude');
|
|
18
|
+
const testSessionSearch = require('./test-session-search');
|
|
19
|
+
const testSessions = require('./test-sessions');
|
|
20
|
+
const testOpenclaw = require('./test-openclaw');
|
|
21
|
+
const testHealthSpeed = require('./test-health-speed');
|
|
129
22
|
|
|
130
23
|
async function main() {
|
|
24
|
+
const realHome = os.homedir();
|
|
25
|
+
const realCodexDir = path.join(realHome, '.codex');
|
|
26
|
+
const realFileStates = [
|
|
27
|
+
captureFileState(path.join(realCodexDir, 'config.toml')),
|
|
28
|
+
captureFileState(path.join(realCodexDir, 'auth.json')),
|
|
29
|
+
captureFileState(path.join(realCodexDir, 'models.json')),
|
|
30
|
+
captureFileState(path.join(realCodexDir, 'provider-current-models.json'))
|
|
31
|
+
];
|
|
32
|
+
|
|
131
33
|
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-e2e-'));
|
|
132
34
|
const env = {
|
|
133
35
|
...process.env,
|
|
@@ -138,208 +40,103 @@ async function main() {
|
|
|
138
40
|
const cliPath = path.resolve(__dirname, '../../cli.js');
|
|
139
41
|
const node = process.execPath;
|
|
140
42
|
|
|
43
|
+
debug('setup start');
|
|
141
44
|
let mockProvider;
|
|
142
45
|
let noModelsProvider;
|
|
143
46
|
let htmlModelsProvider;
|
|
47
|
+
let authFailProvider;
|
|
48
|
+
let webServer;
|
|
144
49
|
try {
|
|
145
50
|
mockProvider = await startLocalServer({ mode: 'list', modelsPath: '/v1/models' });
|
|
146
51
|
noModelsProvider = await startLocalServer({ mode: 'none', modelsPath: '/v1/models' });
|
|
147
52
|
htmlModelsProvider = await startLocalServer({ mode: 'html', modelsPath: '/v1/models' });
|
|
53
|
+
authFailProvider = await startLocalServer({ mode: 'list', modelsPath: '/v1/models', status: 401 });
|
|
54
|
+
|
|
148
55
|
const mockProviderUrl = `http://127.0.0.1:${mockProvider.port}`;
|
|
149
56
|
const noModelsUrl = `http://127.0.0.1:${noModelsProvider.port}`;
|
|
150
57
|
const htmlModelsUrl = `http://127.0.0.1:${htmlModelsProvider.port}`;
|
|
58
|
+
const authFailUrl = `http://127.0.0.1:${authFailProvider.port}`;
|
|
151
59
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
60
|
+
const ctx = {
|
|
61
|
+
env,
|
|
62
|
+
node,
|
|
63
|
+
cliPath,
|
|
64
|
+
tmpHome,
|
|
155
65
|
mockProviderUrl,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
assert(fs.existsSync(configPath), 'config.toml missing');
|
|
167
|
-
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
168
|
-
assert(/model_provider\s*=\s*"e2e"/.test(configContent), 'model_provider not set');
|
|
169
|
-
assert(/model\s*=\s*"e2e-model"/.test(configContent), 'model not set');
|
|
170
|
-
assert(/\[model_providers\.e2e\]/.test(configContent), 'provider block missing');
|
|
171
|
-
assert(/base_url\s*=\s*"http:\/\/127\.0\.0\.1:\d+"/.test(configContent), 'base_url missing');
|
|
172
|
-
|
|
173
|
-
const authPath = path.join(tmpHome, '.codex', 'auth.json');
|
|
174
|
-
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
175
|
-
assert(auth.OPENAI_API_KEY === 'sk-test', 'auth api_key mismatch');
|
|
176
|
-
|
|
177
|
-
const modelsPath = path.join(tmpHome, '.codex', 'models.json');
|
|
178
|
-
const models = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
|
|
179
|
-
assert(models.includes('e2e-model'), 'custom model not added');
|
|
180
|
-
|
|
181
|
-
const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
|
|
182
|
-
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
183
|
-
const sessionId = 'e2e-session';
|
|
184
|
-
const sessionPath = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
185
|
-
const sessionRecords = [
|
|
186
|
-
{
|
|
187
|
-
type: 'session_meta',
|
|
188
|
-
payload: { id: sessionId, cwd: '/tmp/e2e' },
|
|
189
|
-
timestamp: '2025-01-01T00:00:00.000Z'
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
type: 'response_item',
|
|
193
|
-
payload: { type: 'message', role: 'user', content: 'hello' },
|
|
194
|
-
timestamp: '2025-01-01T00:00:01.000Z'
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
type: 'response_item',
|
|
198
|
-
payload: { type: 'message', role: 'assistant', content: 'world' },
|
|
199
|
-
timestamp: '2025-01-01T00:00:02.000Z'
|
|
200
|
-
}
|
|
201
|
-
];
|
|
202
|
-
fs.writeFileSync(sessionPath, sessionRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
|
|
203
|
-
|
|
204
|
-
const statusResult = runSync(node, [cliPath, 'status'], { env });
|
|
205
|
-
assert(statusResult.status === 0, 'status failed');
|
|
206
|
-
assert(statusResult.stdout.includes('提供商: e2e'), 'status provider not shown');
|
|
207
|
-
assert(statusResult.stdout.includes('模型: e2e-model'), 'status model not shown');
|
|
208
|
-
|
|
209
|
-
const listResult = runSync(node, [cliPath, 'list'], { env });
|
|
210
|
-
assert(listResult.status === 0, 'list failed');
|
|
211
|
-
assert(listResult.stdout.includes('e2e'), 'list missing provider');
|
|
66
|
+
noModelsUrl,
|
|
67
|
+
htmlModelsUrl,
|
|
68
|
+
authFailUrl
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await testSetup(ctx);
|
|
72
|
+
if (ctx.skipE2E) {
|
|
73
|
+
console.warn(`E2E skipped: ${ctx.skipE2E}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
212
76
|
|
|
213
77
|
const port = 18000 + Math.floor(Math.random() * 1000);
|
|
214
|
-
|
|
215
|
-
env: { ...env, CODEXMATE_PORT: String(port) },
|
|
216
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
217
|
-
});
|
|
218
|
-
webServer.stdout.on('data', () => {});
|
|
219
|
-
webServer.stderr.on('data', () => {});
|
|
220
|
-
|
|
78
|
+
debug('start web server');
|
|
221
79
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const apiList = await postJson(port, { action: 'list' });
|
|
227
|
-
assert(Array.isArray(apiList.providers), 'api list missing providers');
|
|
228
|
-
assert(apiList.providers.some(p => p.name === 'e2e'), 'api list missing provider');
|
|
229
|
-
|
|
230
|
-
const exportResult = await postJson(port, { action: 'export-config', params: { includeKeys: true } });
|
|
231
|
-
assert(exportResult.data, 'export-config missing data');
|
|
232
|
-
assert(exportResult.data.providers && exportResult.data.providers.e2e, 'export-config missing provider');
|
|
233
|
-
assert(exportResult.data.providers.e2e.apiKey === 'sk-test', 'export-config apiKey mismatch');
|
|
234
|
-
|
|
235
|
-
const importPayload = JSON.parse(JSON.stringify(exportResult.data));
|
|
236
|
-
importPayload.providers = {
|
|
237
|
-
...importPayload.providers,
|
|
238
|
-
e2e2: { baseUrl: mockProviderUrl, apiKey: 'sk-e2e2' },
|
|
239
|
-
e2e3: { baseUrl: noModelsUrl, apiKey: 'sk-e2e3' },
|
|
240
|
-
e2e4: { baseUrl: htmlModelsUrl, apiKey: 'sk-e2e4' }
|
|
241
|
-
};
|
|
242
|
-
importPayload.models = Array.from(new Set([...(importPayload.models || []), 'e2e2-model']));
|
|
243
|
-
importPayload.currentProvider = 'e2e2';
|
|
244
|
-
importPayload.currentModel = 'e2e2-model';
|
|
245
|
-
importPayload.currentModels = { ...(importPayload.currentModels || {}), e2e2: 'e2e2-model' };
|
|
246
|
-
|
|
247
|
-
const importResult = await postJson(port, {
|
|
248
|
-
action: 'import-config',
|
|
249
|
-
params: {
|
|
250
|
-
payload: importPayload,
|
|
251
|
-
options: { overwriteProviders: true, applyCurrent: true, applyCurrentModels: true }
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
assert(importResult.success === true, 'import-config failed');
|
|
255
|
-
|
|
256
|
-
const apiStatusAfter = await postJson(port, { action: 'status' });
|
|
257
|
-
assert(apiStatusAfter.provider === 'e2e2', 'api status provider after import mismatch');
|
|
258
|
-
assert(apiStatusAfter.model === 'e2e2-model', 'api status model after import mismatch');
|
|
259
|
-
|
|
260
|
-
const apiModels = await postJson(port, { action: 'models', params: { provider: 'e2e2' } });
|
|
261
|
-
assert(Array.isArray(apiModels.models) && apiModels.models.includes('e2e2-model-2'), 'api models missing remote entry');
|
|
262
|
-
|
|
263
|
-
const apiModelsUnlimited = await postJson(port, { action: 'models', params: { provider: 'e2e3' } });
|
|
264
|
-
assert(apiModelsUnlimited.unlimited === true, 'api models unlimited missing');
|
|
265
|
-
|
|
266
|
-
const apiModelsHtml = await postJson(port, { action: 'models', params: { provider: 'e2e4' } });
|
|
267
|
-
assert(apiModelsHtml.unlimited === true, 'api models html unlimited missing');
|
|
268
|
-
|
|
269
|
-
const apiModelsByUrl = await postJson(port, {
|
|
270
|
-
action: 'models-by-url',
|
|
271
|
-
params: { baseUrl: mockProviderUrl, apiKey: 'sk-e2e2' }
|
|
272
|
-
});
|
|
273
|
-
assert(Array.isArray(apiModelsByUrl.models) && apiModelsByUrl.models.includes('e2e2-model'), 'api models-by-url missing remote entry');
|
|
274
|
-
|
|
275
|
-
const apiModelsByUrlUnlimited = await postJson(port, {
|
|
276
|
-
action: 'models-by-url',
|
|
277
|
-
params: { baseUrl: noModelsUrl }
|
|
278
|
-
});
|
|
279
|
-
assert(apiModelsByUrlUnlimited.unlimited === true, 'api models-by-url unlimited missing');
|
|
280
|
-
|
|
281
|
-
const apiSessions = await postJson(port, {
|
|
282
|
-
action: 'list-sessions',
|
|
283
|
-
params: { source: 'codex', limit: 50, forceRefresh: true }
|
|
80
|
+
webServer = spawn(node, [cliPath, 'run'], {
|
|
81
|
+
env: { ...env, CODEXMATE_PORT: String(port) },
|
|
82
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
284
83
|
});
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
}
|
|
92
|
+
webServer.stdout.on('data', () => {});
|
|
93
|
+
webServer.stderr.on('data', () => {});
|
|
287
94
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
params: { source: 'codex', sessionId }
|
|
291
|
-
});
|
|
292
|
-
assert(cloneResult.success === true, 'clone-session failed');
|
|
293
|
-
assert(cloneResult.sessionId && cloneResult.sessionId !== sessionId, 'clone-session id invalid');
|
|
294
|
-
assert(fs.existsSync(cloneResult.filePath), 'clone-session file missing');
|
|
95
|
+
await waitForServer(port);
|
|
96
|
+
debug('server ready');
|
|
295
97
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
params: { source: 'codex', limit: 50, forceRefresh: true }
|
|
299
|
-
});
|
|
300
|
-
assert(Array.isArray(apiSessionsAfterClone.sessions), 'api sessions after clone missing');
|
|
301
|
-
assert(
|
|
302
|
-
apiSessionsAfterClone.sessions[0]
|
|
303
|
-
&& apiSessionsAfterClone.sessions[0].sessionId === cloneResult.sessionId,
|
|
304
|
-
'clone session not latest'
|
|
305
|
-
);
|
|
98
|
+
const api = (action, params, timeoutMs) => postJson(port, { action, params }, timeoutMs);
|
|
99
|
+
Object.assign(ctx, { port, api });
|
|
306
100
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
101
|
+
await testConfig(ctx);
|
|
102
|
+
await testClaude(ctx);
|
|
103
|
+
await testSessionSearch(ctx);
|
|
104
|
+
await testSessions(ctx);
|
|
105
|
+
await testOpenclaw(ctx);
|
|
106
|
+
await testHealthSpeed(ctx);
|
|
313
107
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
108
|
+
} finally {
|
|
109
|
+
const waitForExit = new Promise((resolve) => {
|
|
110
|
+
if (!webServer) return resolve();
|
|
111
|
+
const forceKill = setTimeout(() => {
|
|
112
|
+
try {
|
|
113
|
+
webServer.kill('SIGKILL');
|
|
114
|
+
} catch (e) {}
|
|
115
|
+
}, 2000);
|
|
116
|
+
webServer.once('exit', () => {
|
|
117
|
+
clearTimeout(forceKill);
|
|
118
|
+
resolve();
|
|
317
119
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
120
|
+
if (webServer.exitCode !== null || webServer.signalCode) {
|
|
121
|
+
clearTimeout(forceKill);
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
if (webServer) {
|
|
127
|
+
webServer.kill('SIGINT');
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {}
|
|
130
|
+
await waitForExit;
|
|
323
131
|
|
|
324
|
-
|
|
325
|
-
|
|
132
|
+
await closeServer(mockProvider && mockProvider.server);
|
|
133
|
+
await closeServer(noModelsProvider && noModelsProvider.server);
|
|
134
|
+
await closeServer(htmlModelsProvider && htmlModelsProvider.server);
|
|
135
|
+
await closeServer(authFailProvider && authFailProvider.server);
|
|
326
136
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
} finally {
|
|
331
|
-
webServer.kill('SIGINT');
|
|
332
|
-
await new Promise(resolve => webServer.on('exit', resolve));
|
|
333
|
-
}
|
|
334
|
-
} finally {
|
|
335
|
-
if (mockProvider) {
|
|
336
|
-
await closeServer(mockProvider.server);
|
|
337
|
-
}
|
|
338
|
-
if (noModelsProvider) {
|
|
339
|
-
await closeServer(noModelsProvider.server);
|
|
340
|
-
}
|
|
341
|
-
if (htmlModelsProvider) {
|
|
342
|
-
await closeServer(htmlModelsProvider.server);
|
|
137
|
+
for (const state of realFileStates) {
|
|
138
|
+
const label = state && state.path ? path.basename(state.path) : 'real file';
|
|
139
|
+
assertFileUnchanged(state, `real ${label}`);
|
|
343
140
|
}
|
|
344
141
|
try {
|
|
345
142
|
if (fs.rmSync) {
|