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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { assert } = require('./helpers');
|
|
2
|
+
|
|
3
|
+
module.exports = async function testClaude(ctx) {
|
|
4
|
+
const { api, mockProviderUrl, claudeModel } = ctx;
|
|
5
|
+
|
|
6
|
+
const claudeSettingsInfo = await api('get-claude-settings');
|
|
7
|
+
assert(claudeSettingsInfo.apiKey === 'sk-claude', 'get-claude-settings apiKey mismatch');
|
|
8
|
+
assert(claudeSettingsInfo.baseUrl === mockProviderUrl, 'get-claude-settings baseUrl mismatch');
|
|
9
|
+
assert(claudeSettingsInfo.model === claudeModel, 'get-claude-settings model mismatch');
|
|
10
|
+
|
|
11
|
+
const claudeShareMissing = await api('export-claude-share', { config: { apiKey: 'only-key' } });
|
|
12
|
+
assert(claudeShareMissing.error, 'export-claude-share should fail when baseUrl missing');
|
|
13
|
+
|
|
14
|
+
const claudeShare = await api('export-claude-share', {
|
|
15
|
+
config: { baseUrl: mockProviderUrl, apiKey: 'sk-claude', model: claudeModel }
|
|
16
|
+
});
|
|
17
|
+
assert(claudeShare.payload, 'export-claude-share missing payload');
|
|
18
|
+
assert(claudeShare.payload.baseUrl === mockProviderUrl, 'export-claude-share baseUrl mismatch');
|
|
19
|
+
assert(claudeShare.payload.apiKey === 'sk-claude', 'export-claude-share apiKey mismatch');
|
|
20
|
+
assert(claudeShare.payload.model === claudeModel, 'export-claude-share model mismatch');
|
|
21
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { assert } = require('./helpers');
|
|
2
|
+
|
|
3
|
+
module.exports = async function testConfig(ctx) {
|
|
4
|
+
const {
|
|
5
|
+
api,
|
|
6
|
+
mockProviderUrl,
|
|
7
|
+
noModelsUrl,
|
|
8
|
+
htmlModelsUrl,
|
|
9
|
+
authFailUrl
|
|
10
|
+
} = ctx;
|
|
11
|
+
|
|
12
|
+
const apiStatus = await api('status');
|
|
13
|
+
assert(apiStatus.provider === 'e2e', 'api status provider mismatch');
|
|
14
|
+
|
|
15
|
+
const apiList = await api('list');
|
|
16
|
+
assert(Array.isArray(apiList.providers), 'api list missing providers');
|
|
17
|
+
assert(apiList.providers.some(p => p.name === 'e2e'), 'api list missing provider');
|
|
18
|
+
|
|
19
|
+
const templateOverride = await api('get-config-template', {
|
|
20
|
+
provider: 'shadow',
|
|
21
|
+
model: 'shadow-model',
|
|
22
|
+
serviceTier: 'fast'
|
|
23
|
+
});
|
|
24
|
+
assert(typeof templateOverride.template === 'string', 'get-config-template missing template');
|
|
25
|
+
assert(/^\s*service_tier\s*=\s*"fast"\s*$/m.test(templateOverride.template), 'get-config-template missing service_tier');
|
|
26
|
+
assert(templateOverride.template.includes('model_provider = "shadow"'), 'get-config-template missing provider override');
|
|
27
|
+
assert(templateOverride.template.includes('model = "shadow-model"'), 'get-config-template missing model override');
|
|
28
|
+
|
|
29
|
+
const templateStandard = await api('get-config-template', {
|
|
30
|
+
provider: 'shadow',
|
|
31
|
+
model: 'shadow-model',
|
|
32
|
+
serviceTier: 'standard'
|
|
33
|
+
});
|
|
34
|
+
assert(typeof templateStandard.template === 'string', 'get-config-template(standard) missing template');
|
|
35
|
+
assert(!/\s*service_tier\s*=/.test(templateStandard.template), 'get-config-template(standard) should not include service_tier');
|
|
36
|
+
|
|
37
|
+
const exportResult = await api('export-config', { includeKeys: true });
|
|
38
|
+
assert(exportResult.data, 'export-config missing data');
|
|
39
|
+
assert(exportResult.data.providers && exportResult.data.providers.e2e, 'export-config missing provider');
|
|
40
|
+
assert(exportResult.data.providers.e2e.apiKey === 'sk-test', 'export-config apiKey mismatch');
|
|
41
|
+
|
|
42
|
+
const exportNoKeys = await api('export-config', { includeKeys: false });
|
|
43
|
+
assert(exportNoKeys.data, 'export-config(no keys) missing data');
|
|
44
|
+
assert(exportNoKeys.data.providers && exportNoKeys.data.providers.e2e, 'export-config(no keys) missing provider');
|
|
45
|
+
assert(exportNoKeys.data.providers.e2e.apiKey === null, 'export-config(no keys) apiKey should be null');
|
|
46
|
+
|
|
47
|
+
const importInvalid = await api('import-config', { payload: null });
|
|
48
|
+
assert(importInvalid.error && importInvalid.error.includes('Invalid import payload'), 'import-config should reject invalid payload');
|
|
49
|
+
|
|
50
|
+
const modelsMissing = await api('models', { provider: 'missing' });
|
|
51
|
+
assert(modelsMissing.error, 'models should fail for missing provider');
|
|
52
|
+
|
|
53
|
+
const modelsByUrlInvalid = await api('models-by-url', { baseUrl: 'not-a-url' });
|
|
54
|
+
assert(modelsByUrlInvalid.error, 'models-by-url should fail for invalid url');
|
|
55
|
+
|
|
56
|
+
const applyEmpty = await api('apply-config-template', { template: '' });
|
|
57
|
+
assert(applyEmpty.error, 'apply-config-template should reject empty template');
|
|
58
|
+
|
|
59
|
+
const applyNoProvider = await api('apply-config-template', {
|
|
60
|
+
template: 'model = "x"\n[model_providers.x]\nbase_url = "http://example.com"\n'
|
|
61
|
+
});
|
|
62
|
+
assert(applyNoProvider.error, 'apply-config-template should require model_provider');
|
|
63
|
+
|
|
64
|
+
const applyNoModel = await api('apply-config-template', {
|
|
65
|
+
template: 'model_provider = "x"\n[model_providers.x]\nbase_url = "http://example.com"\n'
|
|
66
|
+
});
|
|
67
|
+
assert(applyNoModel.error, 'apply-config-template should require model');
|
|
68
|
+
|
|
69
|
+
const applyNoProviders = await api('apply-config-template', {
|
|
70
|
+
template: 'model_provider = "x"\nmodel = "y"\n'
|
|
71
|
+
});
|
|
72
|
+
assert(applyNoProviders.error, 'apply-config-template should require model_providers');
|
|
73
|
+
|
|
74
|
+
const importPayload = JSON.parse(JSON.stringify(exportResult.data));
|
|
75
|
+
importPayload.providers = {
|
|
76
|
+
...importPayload.providers,
|
|
77
|
+
e2e2: { baseUrl: mockProviderUrl, apiKey: 'sk-e2e2' },
|
|
78
|
+
e2e3: { baseUrl: noModelsUrl, apiKey: 'sk-e2e3' },
|
|
79
|
+
e2e4: { baseUrl: htmlModelsUrl, apiKey: 'sk-e2e4' }
|
|
80
|
+
};
|
|
81
|
+
importPayload.models = Array.from(new Set([...(importPayload.models || []), 'e2e2-model']));
|
|
82
|
+
importPayload.currentProvider = 'e2e2';
|
|
83
|
+
importPayload.currentModel = 'e2e2-model';
|
|
84
|
+
importPayload.currentModels = { ...(importPayload.currentModels || {}), e2e2: 'e2e2-model' };
|
|
85
|
+
|
|
86
|
+
const importResult = await api('import-config', {
|
|
87
|
+
payload: importPayload,
|
|
88
|
+
options: { overwriteProviders: true, applyCurrent: true, applyCurrentModels: true }
|
|
89
|
+
});
|
|
90
|
+
assert(importResult.success === true, 'import-config failed');
|
|
91
|
+
|
|
92
|
+
const exportProviderMissing = await api('export-provider', { name: 'ghost' });
|
|
93
|
+
assert(exportProviderMissing.error, 'export-provider should fail for missing provider');
|
|
94
|
+
|
|
95
|
+
const exportProvider = await api('export-provider', { name: 'e2e2' });
|
|
96
|
+
assert(exportProvider.payload, 'export-provider missing payload');
|
|
97
|
+
assert(exportProvider.payload.baseUrl === mockProviderUrl, 'export-provider baseUrl mismatch');
|
|
98
|
+
assert(exportProvider.payload.apiKey === 'sk-e2e2', 'export-provider apiKey mismatch');
|
|
99
|
+
|
|
100
|
+
const apiStatusAfter = await api('status');
|
|
101
|
+
assert(apiStatusAfter.provider === 'e2e2', 'api status provider after import mismatch');
|
|
102
|
+
assert(apiStatusAfter.model === 'e2e2-model', 'api status model after import mismatch');
|
|
103
|
+
|
|
104
|
+
const apiModels = await api('models', { provider: 'e2e2' });
|
|
105
|
+
assert(Array.isArray(apiModels.models) && apiModels.models.includes('e2e2-model-2'), 'api models missing remote entry');
|
|
106
|
+
|
|
107
|
+
const apiModelsUnlimited = await api('models', { provider: 'e2e3' });
|
|
108
|
+
assert(apiModelsUnlimited.unlimited === true, 'api models unlimited missing');
|
|
109
|
+
|
|
110
|
+
const apiModelsHtml = await api('models', { provider: 'e2e4' });
|
|
111
|
+
assert(apiModelsHtml.unlimited === true, 'api models html unlimited missing');
|
|
112
|
+
|
|
113
|
+
const apiModelsByUrl = await api('models-by-url', { baseUrl: mockProviderUrl, apiKey: 'sk-e2e2' });
|
|
114
|
+
assert(Array.isArray(apiModelsByUrl.models) && apiModelsByUrl.models.includes('e2e2-model'), 'api models-by-url missing remote entry');
|
|
115
|
+
|
|
116
|
+
const apiModelsByUrlUnlimited = await api('models-by-url', { baseUrl: noModelsUrl });
|
|
117
|
+
assert(apiModelsByUrlUnlimited.unlimited === true, 'api models-by-url unlimited missing');
|
|
118
|
+
|
|
119
|
+
const apiPaths = await api('list-session-paths', { source: 'codex', limit: 10, forceRefresh: true });
|
|
120
|
+
assert(Array.isArray(apiPaths.paths), 'api session paths missing');
|
|
121
|
+
assert(apiPaths.paths.includes('/tmp/e2e'), 'api session paths missing cwd');
|
|
122
|
+
|
|
123
|
+
ctx.importPayload = importPayload;
|
|
124
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const {
|
|
3
|
+
assert,
|
|
4
|
+
normalizeWireApi,
|
|
5
|
+
buildModelProbeSpec,
|
|
6
|
+
fileMode,
|
|
7
|
+
writeJsonAtomic
|
|
8
|
+
} = require('./helpers');
|
|
9
|
+
|
|
10
|
+
module.exports = async function testHealthAndSpeed(ctx) {
|
|
11
|
+
const { api, mockProviderUrl, authFailUrl, tmpHome } = ctx;
|
|
12
|
+
|
|
13
|
+
const speedResult = await api('speed-test', { name: 'e2e2' });
|
|
14
|
+
assert(speedResult.ok === true, 'speed-test failed');
|
|
15
|
+
|
|
16
|
+
const configPath = path.join(tmpHome, '.codex', 'config.toml');
|
|
17
|
+
const originalConfig = require('fs').readFileSync(configPath, 'utf-8');
|
|
18
|
+
try {
|
|
19
|
+
const invalidConfig = [
|
|
20
|
+
'model_provider = "bad"',
|
|
21
|
+
'model = "missing"',
|
|
22
|
+
'',
|
|
23
|
+
'[model_providers.bad]',
|
|
24
|
+
'base_url = "not-a-url"',
|
|
25
|
+
'preferred_auth_method = "sk-bad"',
|
|
26
|
+
''
|
|
27
|
+
].join('\n');
|
|
28
|
+
require('fs').writeFileSync(configPath, invalidConfig, 'utf-8');
|
|
29
|
+
|
|
30
|
+
const healthInvalid = await api('config-health-check', { remote: false });
|
|
31
|
+
assert(healthInvalid.ok === false, 'health-check should fail for invalid base_url');
|
|
32
|
+
assert(
|
|
33
|
+
Array.isArray(healthInvalid.issues) &&
|
|
34
|
+
healthInvalid.issues.some(issue => issue.code === 'base-url-invalid'),
|
|
35
|
+
'health-check should report base-url-invalid'
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const healthRemote = await api('config-health-check', { remote: true });
|
|
39
|
+
assert(healthRemote.ok === false, 'health-check(remote) should fail');
|
|
40
|
+
assert(
|
|
41
|
+
Array.isArray(healthRemote.issues) &&
|
|
42
|
+
healthRemote.issues.some(issue => issue.code === 'remote-skip-base-url'),
|
|
43
|
+
'health-check(remote) should report remote-skip-base-url'
|
|
44
|
+
);
|
|
45
|
+
} finally {
|
|
46
|
+
require('fs').writeFileSync(configPath, originalConfig, 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const speedInvalid = await api('speed-test', { url: 'not-a-url' });
|
|
50
|
+
assert(speedInvalid.ok === false || speedInvalid.error, 'speed-test invalid url should fail');
|
|
51
|
+
|
|
52
|
+
const speedAuthFail = await api('speed-test', { url: authFailUrl });
|
|
53
|
+
assert(
|
|
54
|
+
speedAuthFail.status === 401 || (speedAuthFail.error && /401/.test(speedAuthFail.error)),
|
|
55
|
+
'speed-test auth fail should expose 401'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const speedUnreachable = await api('speed-test', { url: 'http://127.0.0.1:1' });
|
|
59
|
+
assert(speedUnreachable.ok === false || speedUnreachable.error, 'speed-test unreachable should fail');
|
|
60
|
+
|
|
61
|
+
assert(normalizeWireApi('chat/completions') === 'chat_completions', 'normalizeWireApi should replace "/" with "_"');
|
|
62
|
+
const probeSpec = buildModelProbeSpec({ wire_api: 'chat/completions' }, 'e2e-chat', mockProviderUrl);
|
|
63
|
+
assert(probeSpec && probeSpec.url.endsWith('/chat/completions'), 'buildModelProbeSpec should use chat/completions endpoint for slash wire_api');
|
|
64
|
+
|
|
65
|
+
const permDir = require('fs').mkdtempSync(path.join(tmpHome, 'perm-'));
|
|
66
|
+
const existingPath = path.join(permDir, 'secret.json');
|
|
67
|
+
require('fs').writeFileSync(existingPath, JSON.stringify({ a: 1 }), { mode: 0o640 });
|
|
68
|
+
const origMode = fileMode(existingPath);
|
|
69
|
+
writeJsonAtomic(existingPath, { a: 2 });
|
|
70
|
+
assert(fileMode(existingPath) === origMode, 'writeJsonAtomic should preserve mode of existing file');
|
|
71
|
+
|
|
72
|
+
const newPath = path.join(permDir, 'new.json');
|
|
73
|
+
writeJsonAtomic(newPath, { b: 1 });
|
|
74
|
+
assert(fileMode(newPath) === 0o600, 'writeJsonAtomic should default to 600 for new file');
|
|
75
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { assert } = require('./helpers');
|
|
2
|
+
|
|
3
|
+
module.exports = async function testOpenclaw(ctx) {
|
|
4
|
+
const { api } = ctx;
|
|
5
|
+
|
|
6
|
+
const openclawReadEmpty = await api('get-openclaw-config');
|
|
7
|
+
assert(openclawReadEmpty.exists === false, 'openclaw config should not exist initially');
|
|
8
|
+
|
|
9
|
+
const openclawInvalid = await api('apply-openclaw-config', { content: '', lineEnding: '\n' });
|
|
10
|
+
assert(openclawInvalid.success === false, 'apply-openclaw-config should reject empty content');
|
|
11
|
+
|
|
12
|
+
const openclawContent = [
|
|
13
|
+
'{',
|
|
14
|
+
' "agent": { "model": "gpt-4.1" },',
|
|
15
|
+
' "agents": { "defaults": { "workspace": "~/.openclaw/workspace" } }',
|
|
16
|
+
'}'
|
|
17
|
+
].join('\n');
|
|
18
|
+
const openclawApply = await api('apply-openclaw-config', { content: openclawContent, lineEnding: '\n' });
|
|
19
|
+
assert(openclawApply.success === true, `apply-openclaw-config failed${openclawApply && openclawApply.error ? `: ${openclawApply.error}` : ''}`);
|
|
20
|
+
const openclawReadAfter = await api('get-openclaw-config');
|
|
21
|
+
assert(openclawReadAfter.exists === true, 'openclaw config should exist after apply');
|
|
22
|
+
|
|
23
|
+
const openclawAgentsBefore = await api('get-openclaw-agents-file');
|
|
24
|
+
assert(openclawAgentsBefore.path, 'get-openclaw-agents-file missing path');
|
|
25
|
+
const openclawAgentsApply = await api('apply-openclaw-agents-file', { content: 'openclaw-agents', lineEnding: '\n' });
|
|
26
|
+
assert(openclawAgentsApply.success === true, 'apply-openclaw-agents-file failed');
|
|
27
|
+
const openclawAgentsAfter = await api('get-openclaw-agents-file');
|
|
28
|
+
assert(openclawAgentsAfter.exists === true, 'openclaw agents should exist after apply');
|
|
29
|
+
assert(openclawAgentsAfter.content.includes('openclaw-agents'), 'openclaw agents content mismatch');
|
|
30
|
+
|
|
31
|
+
const openclawWorkspaceInvalid = await api('apply-openclaw-workspace-file', {
|
|
32
|
+
fileName: 'bad.txt',
|
|
33
|
+
content: 'x',
|
|
34
|
+
lineEnding: '\n'
|
|
35
|
+
});
|
|
36
|
+
assert(openclawWorkspaceInvalid.error, 'apply-openclaw-workspace-file should reject invalid name');
|
|
37
|
+
|
|
38
|
+
const openclawWorkspaceApply = await api('apply-openclaw-workspace-file', {
|
|
39
|
+
fileName: 'SOUL.md',
|
|
40
|
+
content: 'workspace-content',
|
|
41
|
+
lineEnding: '\n'
|
|
42
|
+
});
|
|
43
|
+
assert(openclawWorkspaceApply.success === true, 'apply-openclaw-workspace-file failed');
|
|
44
|
+
const openclawWorkspaceRead = await api('get-openclaw-workspace-file', { fileName: 'SOUL.md' });
|
|
45
|
+
assert(openclawWorkspaceRead.exists === true, 'get-openclaw-workspace-file missing after apply');
|
|
46
|
+
assert(openclawWorkspaceRead.content.includes('workspace-content'), 'openclaw workspace content mismatch');
|
|
47
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { assert } = require('./helpers');
|
|
3
|
+
|
|
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 apiSessionsAll = await api('list-sessions', { source: 'all', limit: 50, forceRefresh: true });
|
|
12
|
+
assert(Array.isArray(apiSessionsAll.sessions), 'api sessions(all) missing');
|
|
13
|
+
assert(apiSessionsAll.sessions.some(item => item.sessionId === sessionId), 'api sessions(all) missing codex entry');
|
|
14
|
+
|
|
15
|
+
const sessionDetail = await api('session-detail', { source: 'codex', sessionId });
|
|
16
|
+
assert(Array.isArray(sessionDetail.messages), 'session-detail missing messages');
|
|
17
|
+
|
|
18
|
+
const sessionPlain = await api('session-plain', { source: 'codex', sessionId });
|
|
19
|
+
assert(sessionPlain.text && sessionPlain.text.includes('world'), 'session-plain missing content');
|
|
20
|
+
|
|
21
|
+
const sessionPlainMissing = await api('session-plain', { source: 'codex', sessionId: 'missing-session' });
|
|
22
|
+
assert(sessionPlainMissing.error, 'session-plain should fail for missing session');
|
|
23
|
+
|
|
24
|
+
const exportSession = await api('export-session', { source: 'codex', sessionId, maxMessages: 1 });
|
|
25
|
+
assert(exportSession.content, 'export-session missing content');
|
|
26
|
+
assert(exportSession.truncated === true, 'export-session should be truncated with maxMessages');
|
|
27
|
+
|
|
28
|
+
const cloneResult = await api('clone-session', { source: 'codex', sessionId });
|
|
29
|
+
assert(cloneResult.success === true, 'clone-session failed');
|
|
30
|
+
assert(cloneResult.sessionId && cloneResult.sessionId !== sessionId, 'clone-session id invalid');
|
|
31
|
+
assert(cloneResult.filePath && cloneResult.filePath.endsWith('.jsonl'), 'clone-session file path invalid');
|
|
32
|
+
assert(cloneResult.filePath && require('fs').existsSync(cloneResult.filePath), 'clone-session file missing');
|
|
33
|
+
const cloneSessionId = cloneResult.sessionId;
|
|
34
|
+
|
|
35
|
+
const cloneInvalid = await api('clone-session', { source: 'claude', sessionId });
|
|
36
|
+
assert(cloneInvalid.error, 'clone-session should reject non-codex source');
|
|
37
|
+
|
|
38
|
+
const apiSessionsAfterClone = await api('list-sessions', { source: 'codex', limit: 50, forceRefresh: true });
|
|
39
|
+
assert(Array.isArray(apiSessionsAfterClone.sessions), 'api sessions after clone missing');
|
|
40
|
+
assert(
|
|
41
|
+
apiSessionsAfterClone.sessions[0]
|
|
42
|
+
&& apiSessionsAfterClone.sessions[0].sessionId === cloneSessionId,
|
|
43
|
+
'clone session not latest'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const deleteResult = await api('delete-session', { source: 'codex', sessionId });
|
|
47
|
+
assert(deleteResult.success === true, 'delete-session failed');
|
|
48
|
+
|
|
49
|
+
const deleteMissing = await api('delete-session', { source: 'codex', sessionId });
|
|
50
|
+
assert(deleteMissing.error, 'delete-session should fail for missing session');
|
|
51
|
+
|
|
52
|
+
const detailMissing = await api('session-detail', { source: 'codex', sessionId });
|
|
53
|
+
assert(detailMissing.error, 'session-detail should fail after delete');
|
|
54
|
+
|
|
55
|
+
const apiSessionsAfterDelete = await api('list-sessions', { source: 'codex', limit: 50, forceRefresh: true });
|
|
56
|
+
assert(!apiSessionsAfterDelete.sessions.some(item => item.sessionId === sessionId), 'deleted session still listed');
|
|
57
|
+
assert(apiSessionsAfterDelete.sessions.some(item => item.sessionId === cloneSessionId), 'clone session missing after delete');
|
|
58
|
+
|
|
59
|
+
Object.assign(ctx, { cloneSessionId });
|
|
60
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
assert(setupResult.status === 0, `setup failed: ${setupResult.stderr || setupResult.stdout}`);
|
|
23
|
+
|
|
24
|
+
const configPath = path.join(tmpHome, '.codex', 'config.toml');
|
|
25
|
+
assert(fs.existsSync(configPath), 'config.toml missing');
|
|
26
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
27
|
+
assert(/model_provider\s*=\s*"e2e"/.test(configContent), 'model_provider not set');
|
|
28
|
+
assert(/model\s*=\s*"e2e-model"/.test(configContent), 'model not set');
|
|
29
|
+
assert(/\[model_providers\.e2e\]/.test(configContent), 'provider block missing');
|
|
30
|
+
assert(configContent.includes(`base_url = "${mockProviderUrl}"`), 'base_url missing or mismatched');
|
|
31
|
+
|
|
32
|
+
const authPath = path.join(tmpHome, '.codex', 'auth.json');
|
|
33
|
+
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
34
|
+
assert(auth.OPENAI_API_KEY === 'sk-test', 'auth api_key mismatch');
|
|
35
|
+
|
|
36
|
+
const modelsPath = path.join(tmpHome, '.codex', 'models.json');
|
|
37
|
+
const models = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
|
|
38
|
+
assert(models.includes('e2e-model'), 'custom model not added');
|
|
39
|
+
|
|
40
|
+
const statusResult = runSync(node, [cliPath, 'status'], { env });
|
|
41
|
+
assert(statusResult.status === 0, 'status failed');
|
|
42
|
+
assert(statusResult.stdout.includes('e2e'), 'status provider not shown');
|
|
43
|
+
assert(statusResult.stdout.includes('e2e-model'), 'status model not shown');
|
|
44
|
+
|
|
45
|
+
const listResult = runSync(node, [cliPath, 'list'], { env });
|
|
46
|
+
assert(listResult.status === 0, 'list failed');
|
|
47
|
+
assert(listResult.stdout.includes('e2e'), 'list missing provider');
|
|
48
|
+
|
|
49
|
+
const claudeModel = 'claude-e2e';
|
|
50
|
+
const claudeResult = runSync(node, [cliPath, 'claude', mockProviderUrl, 'sk-claude', claudeModel], { env });
|
|
51
|
+
assert(claudeResult.status === 0, `claude command failed: ${claudeResult.stderr || claudeResult.stdout}`);
|
|
52
|
+
const claudeSettingsPath = path.join(tmpHome, '.claude', 'settings.json');
|
|
53
|
+
assert(fs.existsSync(claudeSettingsPath), 'claude settings missing');
|
|
54
|
+
const claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
|
|
55
|
+
assert(claudeSettings.env && claudeSettings.env.ANTHROPIC_API_KEY === 'sk-claude', 'claude API key mismatch');
|
|
56
|
+
assert(claudeSettings.env.ANTHROPIC_BASE_URL === mockProviderUrl, 'claude base url mismatch');
|
|
57
|
+
assert(claudeSettings.env.ANTHROPIC_MODEL === claudeModel, 'claude model mismatch');
|
|
58
|
+
|
|
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 = [
|
|
64
|
+
{
|
|
65
|
+
type: 'session_meta',
|
|
66
|
+
payload: { id: sessionId, cwd: '/tmp/e2e' },
|
|
67
|
+
timestamp: '2025-01-01T00:00:00.000Z'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'response_item',
|
|
71
|
+
payload: { type: 'message', role: 'user', content: 'hello' },
|
|
72
|
+
timestamp: '2025-01-01T00:00:01.000Z'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'response_item',
|
|
76
|
+
payload: { type: 'message', role: 'assistant', content: 'world' },
|
|
77
|
+
timestamp: '2025-01-01T00:00:02.000Z'
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
fs.writeFileSync(sessionPath, sessionRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
|
|
81
|
+
|
|
82
|
+
Object.assign(ctx, {
|
|
83
|
+
claudeModel,
|
|
84
|
+
sessionId,
|
|
85
|
+
sessionPath,
|
|
86
|
+
noModelsUrl,
|
|
87
|
+
htmlModelsUrl,
|
|
88
|
+
authFailUrl
|
|
89
|
+
});
|
|
90
|
+
};
|