codexmate 0.0.6 → 0.0.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.
- package/.github/workflows/release.yml +122 -8
- package/README.md +48 -40
- package/README.zh-CN.md +48 -40
- package/cli.js +784 -1165
- package/lib/cli-file-utils.js +149 -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 +3 -2
- package/tests/e2e/helpers.js +214 -0
- package/tests/e2e/recent-health.e2e.js +6 -0
- package/tests/e2e/run.js +84 -302
- package/tests/e2e/test-claude.js +21 -0
- package/tests/e2e/test-config.js +124 -0
- package/tests/e2e/test-health-speed.js +75 -0
- package/tests/e2e/test-openclaw.js +47 -0
- package/tests/e2e/test-sessions.js +60 -0
- package/tests/e2e/test-setup.js +90 -0
- package/web-ui.html +912 -421
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"name": "codexmate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codexmate": "./cli.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node cli.js",
|
|
10
|
+
"test": "npm run test:e2e",
|
|
10
11
|
"test:e2e": "node tests/e2e/run.js"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
@@ -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,34 @@
|
|
|
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
|
-
return new Promise((resolve) => {
|
|
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 testSessions = require('./test-sessions');
|
|
19
|
+
const testOpenclaw = require('./test-openclaw');
|
|
20
|
+
const testHealthSpeed = require('./test-health-speed');
|
|
129
21
|
|
|
130
22
|
async function main() {
|
|
23
|
+
const realHome = os.homedir();
|
|
24
|
+
const realCodexDir = path.join(realHome, '.codex');
|
|
25
|
+
const realFileStates = [
|
|
26
|
+
captureFileState(path.join(realCodexDir, 'config.toml')),
|
|
27
|
+
captureFileState(path.join(realCodexDir, 'auth.json')),
|
|
28
|
+
captureFileState(path.join(realCodexDir, 'models.json')),
|
|
29
|
+
captureFileState(path.join(realCodexDir, 'provider-current-models.json'))
|
|
30
|
+
];
|
|
31
|
+
|
|
131
32
|
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-e2e-'));
|
|
132
33
|
const env = {
|
|
133
34
|
...process.env,
|
|
@@ -138,208 +39,89 @@ async function main() {
|
|
|
138
39
|
const cliPath = path.resolve(__dirname, '../../cli.js');
|
|
139
40
|
const node = process.execPath;
|
|
140
41
|
|
|
42
|
+
debug('setup start');
|
|
141
43
|
let mockProvider;
|
|
142
44
|
let noModelsProvider;
|
|
143
45
|
let htmlModelsProvider;
|
|
46
|
+
let authFailProvider;
|
|
47
|
+
let webServer;
|
|
144
48
|
try {
|
|
145
49
|
mockProvider = await startLocalServer({ mode: 'list', modelsPath: '/v1/models' });
|
|
146
50
|
noModelsProvider = await startLocalServer({ mode: 'none', modelsPath: '/v1/models' });
|
|
147
51
|
htmlModelsProvider = await startLocalServer({ mode: 'html', modelsPath: '/v1/models' });
|
|
52
|
+
authFailProvider = await startLocalServer({ mode: 'list', modelsPath: '/v1/models', status: 401 });
|
|
53
|
+
|
|
148
54
|
const mockProviderUrl = `http://127.0.0.1:${mockProvider.port}`;
|
|
149
55
|
const noModelsUrl = `http://127.0.0.1:${noModelsProvider.port}`;
|
|
150
56
|
const htmlModelsUrl = `http://127.0.0.1:${htmlModelsProvider.port}`;
|
|
57
|
+
const authFailUrl = `http://127.0.0.1:${authFailProvider.port}`;
|
|
151
58
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
59
|
+
const ctx = {
|
|
60
|
+
env,
|
|
61
|
+
node,
|
|
62
|
+
cliPath,
|
|
63
|
+
tmpHome,
|
|
155
64
|
mockProviderUrl,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const setupResult = await runWithInput(node, [cliPath, 'setup'], setupInput, { env });
|
|
65
|
+
noModelsUrl,
|
|
66
|
+
htmlModelsUrl,
|
|
67
|
+
authFailUrl
|
|
68
|
+
};
|
|
162
69
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const configPath = path.join(tmpHome, '.codex', 'config.toml');
|
|
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');
|
|
70
|
+
await testSetup(ctx);
|
|
212
71
|
|
|
213
72
|
const port = 18000 + Math.floor(Math.random() * 1000);
|
|
214
|
-
|
|
73
|
+
debug('start web server');
|
|
74
|
+
webServer = spawn(node, [cliPath, 'run'], {
|
|
215
75
|
env: { ...env, CODEXMATE_PORT: String(port) },
|
|
216
76
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
217
77
|
});
|
|
218
78
|
webServer.stdout.on('data', () => {});
|
|
219
79
|
webServer.stderr.on('data', () => {});
|
|
220
80
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const apiStatus = await postJson(port, { action: 'status' });
|
|
224
|
-
assert(apiStatus.provider === 'e2e', 'api status provider mismatch');
|
|
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');
|
|
81
|
+
await waitForServer(port);
|
|
82
|
+
debug('server ready');
|
|
262
83
|
|
|
263
|
-
|
|
264
|
-
|
|
84
|
+
const api = (action, params, timeoutMs) => postJson(port, { action, params }, timeoutMs);
|
|
85
|
+
Object.assign(ctx, { port, api });
|
|
265
86
|
|
|
266
|
-
|
|
267
|
-
|
|
87
|
+
await testConfig(ctx);
|
|
88
|
+
await testClaude(ctx);
|
|
89
|
+
await testSessions(ctx);
|
|
90
|
+
await testOpenclaw(ctx);
|
|
91
|
+
await testHealthSpeed(ctx);
|
|
268
92
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const apiSessions = await postJson(port, {
|
|
282
|
-
action: 'list-sessions',
|
|
283
|
-
params: { source: 'codex', limit: 50, forceRefresh: true }
|
|
284
|
-
});
|
|
285
|
-
assert(Array.isArray(apiSessions.sessions), 'api sessions missing');
|
|
286
|
-
assert(apiSessions.sessions.some(item => item.sessionId === sessionId), 'api sessions missing codex entry');
|
|
287
|
-
|
|
288
|
-
const cloneResult = await postJson(port, {
|
|
289
|
-
action: 'clone-session',
|
|
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');
|
|
295
|
-
|
|
296
|
-
const apiSessionsAfterClone = await postJson(port, {
|
|
297
|
-
action: 'list-sessions',
|
|
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
|
-
);
|
|
306
|
-
|
|
307
|
-
const deleteResult = await postJson(port, {
|
|
308
|
-
action: 'delete-session',
|
|
309
|
-
params: { source: 'codex', sessionId }
|
|
310
|
-
});
|
|
311
|
-
assert(deleteResult.success === true, 'delete-session failed');
|
|
312
|
-
assert(!fs.existsSync(sessionPath), 'delete-session file still exists');
|
|
313
|
-
|
|
314
|
-
const apiSessionsAfterDelete = await postJson(port, {
|
|
315
|
-
action: 'list-sessions',
|
|
316
|
-
params: { source: 'codex', limit: 50, forceRefresh: true }
|
|
93
|
+
} finally {
|
|
94
|
+
const waitForExit = new Promise((resolve) => {
|
|
95
|
+
if (!webServer) return resolve();
|
|
96
|
+
const forceKill = setTimeout(() => {
|
|
97
|
+
try {
|
|
98
|
+
webServer.kill('SIGKILL');
|
|
99
|
+
} catch (e) {}
|
|
100
|
+
}, 2000);
|
|
101
|
+
webServer.once('exit', () => {
|
|
102
|
+
clearTimeout(forceKill);
|
|
103
|
+
resolve();
|
|
317
104
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
105
|
+
if (webServer.exitCode !== null || webServer.signalCode) {
|
|
106
|
+
clearTimeout(forceKill);
|
|
107
|
+
resolve();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
if (webServer) {
|
|
112
|
+
webServer.kill('SIGINT');
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {}
|
|
115
|
+
await waitForExit;
|
|
323
116
|
|
|
324
|
-
|
|
325
|
-
|
|
117
|
+
await closeServer(mockProvider && mockProvider.server);
|
|
118
|
+
await closeServer(noModelsProvider && noModelsProvider.server);
|
|
119
|
+
await closeServer(htmlModelsProvider && htmlModelsProvider.server);
|
|
120
|
+
await closeServer(authFailProvider && authFailProvider.server);
|
|
326
121
|
|
|
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);
|
|
122
|
+
for (const state of realFileStates) {
|
|
123
|
+
const label = state && state.path ? path.basename(state.path) : 'real file';
|
|
124
|
+
assertFileUnchanged(state, `real ${label}`);
|
|
343
125
|
}
|
|
344
126
|
try {
|
|
345
127
|
if (fs.rmSync) {
|