codexmate 0.0.9 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +25 -6
  2. package/README.zh-CN.md +25 -6
  3. package/package.json +53 -36
  4. package/res/logo.png +0 -0
  5. package/{cli.js → src/cli.js} +822 -327
  6. package/src/lib/cli-file-utils.js +151 -0
  7. package/src/lib/cli-models-utils.js +152 -0
  8. package/src/lib/cli-network-utils.js +148 -0
  9. package/src/lib/cli-session-utils.js +121 -0
  10. package/src/lib/cli-utils.js +139 -0
  11. package/src/res/json5.min.js +1 -0
  12. package/src/res/logo.png +0 -0
  13. package/src/res/screenshot.png +0 -0
  14. package/src/res/vue.global.js +18552 -0
  15. package/src/web-ui/app.js +2970 -0
  16. package/src/web-ui/index.html +1310 -0
  17. package/src/web-ui/logic.mjs +157 -0
  18. package/src/web-ui/styles.css +2868 -0
  19. package/src/web-ui.html +17 -0
  20. package/web-ui/app.js +273 -144
  21. package/web-ui/index.html +1310 -0
  22. package/web-ui/logic.mjs +21 -21
  23. package/web-ui/styles.css +2868 -0
  24. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  25. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  26. package/.github/workflows/ci.yml +0 -26
  27. package/.github/workflows/release.yml +0 -159
  28. package/.planning/.fix-attempts +0 -1
  29. package/.planning/.lock +0 -6
  30. package/.planning/.verify-cache.json +0 -14
  31. package/.planning/CHECKPOINT.json +0 -46
  32. package/.planning/DESIGN.md +0 -26
  33. package/.planning/HISTORY.json +0 -124
  34. package/.planning/PLAN.md +0 -69
  35. package/.planning/REVIEW.md +0 -41
  36. package/.planning/STATE.md +0 -12
  37. package/.planning/STATS.json +0 -13
  38. package/.planning/VERIFICATION.md +0 -70
  39. package/.planning/daude-code-plan.md +0 -51
  40. package/.planning/research/architecture.md +0 -32
  41. package/.planning/research/conventions.md +0 -36
  42. package/.planning/task_1-REVIEW.md +0 -29
  43. package/.planning/task_1-SUMMARY.md +0 -32
  44. package/.planning/task_2-REVIEW.md +0 -24
  45. package/.planning/task_2-SUMMARY.md +0 -37
  46. package/.planning/task_3-REVIEW.md +0 -25
  47. package/.planning/task_3-SUMMARY.md +0 -31
  48. package/cmd/publish-npm.cmd +0 -65
  49. package/tests/e2e/helpers.js +0 -214
  50. package/tests/e2e/recent-health.e2e.js +0 -142
  51. package/tests/e2e/run.js +0 -154
  52. package/tests/e2e/test-claude.js +0 -21
  53. package/tests/e2e/test-config.js +0 -124
  54. package/tests/e2e/test-health-speed.js +0 -79
  55. package/tests/e2e/test-openclaw.js +0 -47
  56. package/tests/e2e/test-session-search.js +0 -114
  57. package/tests/e2e/test-sessions.js +0 -69
  58. package/tests/e2e/test-setup.js +0 -159
  59. package/tests/unit/run.mjs +0 -29
  60. package/tests/unit/web-ui-logic.test.mjs +0 -186
  61. package/web-ui.html +0 -3977
  62. /package/{CHANGELOG.md → doc/CHANGELOG.md} +0 -0
  63. /package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +0 -0
@@ -1,142 +0,0 @@
1
- const http = require('http');
2
- const path = require('path');
3
- const os = require('os');
4
- const fs = require('fs');
5
- const { spawn } = require('child_process');
6
-
7
- const PORT = 3737;
8
- const API_URL = `http://localhost:${PORT}/api`;
9
-
10
- function delay(ms) {
11
- return new Promise(resolve => setTimeout(resolve, ms));
12
- }
13
-
14
- function request(action, params = {}) {
15
- const payload = JSON.stringify({ action, params });
16
- return new Promise((resolve, reject) => {
17
- const req = http.request(API_URL, {
18
- method: 'POST',
19
- headers: {
20
- 'Content-Type': 'application/json',
21
- 'Content-Length': Buffer.byteLength(payload)
22
- }
23
- }, (res) => {
24
- let data = '';
25
- res.on('data', chunk => data += chunk);
26
- res.on('end', () => {
27
- try {
28
- const json = JSON.parse(data || '{}');
29
- resolve(json);
30
- } catch (e) {
31
- reject(e);
32
- }
33
- });
34
- });
35
- req.on('error', reject);
36
- req.write(payload);
37
- req.end();
38
- });
39
- }
40
-
41
- async function waitForServer(timeoutMs = 12000) {
42
- const start = Date.now();
43
- while (Date.now() - start < timeoutMs) {
44
- try {
45
- await request('status');
46
- return;
47
- } catch (_) {
48
- await delay(300);
49
- }
50
- }
51
- throw new Error('server not ready');
52
- }
53
-
54
- function escapeToml(value) {
55
- return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
56
- }
57
-
58
- function buildTemplate(provider, model, baseUrl, apiKey) {
59
- return `model_provider = "${escapeToml(provider)}"
60
- model = "${escapeToml(model)}"
61
-
62
- [model_providers.${escapeToml(provider)}]
63
- name = "${escapeToml(provider)}"
64
- base_url = "${escapeToml(baseUrl)}"
65
- wire_api = "responses"
66
- preferred_auth_method = "${escapeToml(apiKey)}"
67
- `;
68
- }
69
-
70
- function assert(condition, message) {
71
- if (!condition) {
72
- throw new Error(`Assertion failed: ${message}`);
73
- }
74
- }
75
-
76
- async function run() {
77
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-e2e-'));
78
- const env = {
79
- ...process.env,
80
- USERPROFILE: tempHome,
81
- HOME: tempHome,
82
- CODEXMATE_NO_BROWSER: '1'
83
- };
84
-
85
- const cliPath = path.join(__dirname, '..', '..', 'cli.js');
86
- const child = spawn(process.execPath, [cliPath, 'start'], {
87
- env,
88
- stdio: ['ignore', 'pipe', 'pipe']
89
- });
90
-
91
- try {
92
- await waitForServer();
93
-
94
- await request('apply-config-template', { template: buildTemplate('alpha', 'm-alpha', 'https://example.com', 'sk-alpha') });
95
- await request('apply-config-template', { template: buildTemplate('beta', 'm-beta', 'https://example.com', 'sk-beta') });
96
- await request('apply-config-template', { template: buildTemplate('gamma', 'm-gamma', 'https://example.com', 'sk-gamma') });
97
- await request('apply-config-template', { template: buildTemplate('delta', 'm-delta', 'https://example.com', 'sk-delta') });
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
-
105
- const recent = await request('get-recent-configs');
106
- assert(Array.isArray(recent.items), 'recent list should be array');
107
- assert(recent.items.length === 3, 'recent list should keep 3 items');
108
- assert(recent.items[0].provider === 'delta' && recent.items[0].model === 'm-delta', 'recent[0] should be delta');
109
- assert(recent.items[1].provider === 'gamma' && recent.items[1].model === 'm-gamma', 'recent[1] should be gamma');
110
- assert(recent.items[2].provider === 'beta' && recent.items[2].model === 'm-beta', 'recent[2] should be beta');
111
-
112
- await request('apply-config-template', { template: buildTemplate('broken', 'm-broken', 'not-a-url', '') });
113
- await request('delete-model', { model: 'm-broken' });
114
-
115
- const health = await request('config-health-check');
116
- assert(health && Array.isArray(health.issues), 'health check issues should be array');
117
- const codes = new Set(health.issues.map(item => item.code));
118
- assert(codes.has('base-url-invalid'), 'health check should flag invalid URL');
119
- assert(codes.has('api-key-missing'), 'health check should flag missing key');
120
- assert(codes.has('model-unavailable'), 'health check should flag missing model');
121
-
122
- await request('apply-config-template', {
123
- template: buildTemplate('local', 'm-local', `http://127.0.0.1:${PORT}`, 'sk-local')
124
- });
125
- const remoteHealth = await request('config-health-check', { remote: true, timeoutMs: 2000 });
126
- assert(remoteHealth && Array.isArray(remoteHealth.issues), 'remote health issues should be array');
127
- assert(remoteHealth.ok === true, 'remote health should pass');
128
- assert(remoteHealth.remote && remoteHealth.remote.type === 'speed-test', 'remote health should include speed-test info');
129
- assert(typeof remoteHealth.remote.durationMs === 'number', 'remote health should include duration');
130
- } finally {
131
- if (child && !child.killed) {
132
- child.kill('SIGINT');
133
- }
134
- await delay(300);
135
- fs.rmSync(tempHome, { recursive: true, force: true });
136
- }
137
- }
138
-
139
- run().catch((err) => {
140
- console.error(err);
141
- process.exit(1);
142
- });
package/tests/e2e/run.js DELETED
@@ -1,154 +0,0 @@
1
- const { spawn } = require('child_process');
2
- const path = require('path');
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');
22
-
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
-
33
- const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-e2e-'));
34
- const env = {
35
- ...process.env,
36
- HOME: tmpHome,
37
- USERPROFILE: tmpHome,
38
- CODEXMATE_FORCE_RESET_EXISTING_CONFIG: '1'
39
- };
40
- const cliPath = path.resolve(__dirname, '../../cli.js');
41
- const node = process.execPath;
42
-
43
- debug('setup start');
44
- let mockProvider;
45
- let noModelsProvider;
46
- let htmlModelsProvider;
47
- let authFailProvider;
48
- let webServer;
49
- try {
50
- mockProvider = await startLocalServer({ mode: 'list', modelsPath: '/v1/models' });
51
- noModelsProvider = await startLocalServer({ mode: 'none', modelsPath: '/v1/models' });
52
- htmlModelsProvider = await startLocalServer({ mode: 'html', modelsPath: '/v1/models' });
53
- authFailProvider = await startLocalServer({ mode: 'list', modelsPath: '/v1/models', status: 401 });
54
-
55
- const mockProviderUrl = `http://127.0.0.1:${mockProvider.port}`;
56
- const noModelsUrl = `http://127.0.0.1:${noModelsProvider.port}`;
57
- const htmlModelsUrl = `http://127.0.0.1:${htmlModelsProvider.port}`;
58
- const authFailUrl = `http://127.0.0.1:${authFailProvider.port}`;
59
-
60
- const ctx = {
61
- env,
62
- node,
63
- cliPath,
64
- tmpHome,
65
- mockProviderUrl,
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
- }
76
-
77
- const port = 18000 + Math.floor(Math.random() * 1000);
78
- debug('start web server');
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
- }
92
- webServer.stdout.on('data', () => {});
93
- webServer.stderr.on('data', () => {});
94
-
95
- await waitForServer(port);
96
- debug('server ready');
97
-
98
- const api = (action, params, timeoutMs) => postJson(port, { action, params }, timeoutMs);
99
- Object.assign(ctx, { port, api });
100
-
101
- await testConfig(ctx);
102
- await testClaude(ctx);
103
- await testSessionSearch(ctx);
104
- await testSessions(ctx);
105
- await testOpenclaw(ctx);
106
- await testHealthSpeed(ctx);
107
-
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();
119
- });
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;
131
-
132
- await closeServer(mockProvider && mockProvider.server);
133
- await closeServer(noModelsProvider && noModelsProvider.server);
134
- await closeServer(htmlModelsProvider && htmlModelsProvider.server);
135
- await closeServer(authFailProvider && authFailProvider.server);
136
-
137
- for (const state of realFileStates) {
138
- const label = state && state.path ? path.basename(state.path) : 'real file';
139
- assertFileUnchanged(state, `real ${label}`);
140
- }
141
- try {
142
- if (fs.rmSync) {
143
- fs.rmSync(tmpHome, { recursive: true, force: true });
144
- } else {
145
- fs.rmdirSync(tmpHome, { recursive: true });
146
- }
147
- } catch (e) {}
148
- }
149
- }
150
-
151
- main().catch((err) => {
152
- console.error('E2E failed:', err.message || err);
153
- process.exit(1);
154
- });
@@ -1,21 +0,0 @@
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
- };
@@ -1,124 +0,0 @@
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
- };
@@ -1,79 +0,0 @@
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
- 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
- );
79
- };
@@ -1,47 +0,0 @@
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
- };