@textcortex/zenocode 0.1.13 → 0.1.15
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/README.md +48 -3
- package/package.json +8 -3
- package/scripts/build-branded-opencode.mjs +357 -189
- package/scripts/build-branded-opencode.test.mjs +99 -40
- package/scripts/opencode-version.mjs +5 -3
- package/scripts/run-zenocode.mjs +601 -755
- package/scripts/run-zenocode.test.mjs +648 -390
- package/scripts/source-branding.mjs +608 -0
- package/scripts/source-branding.test.mjs +76 -0
- package/scripts/branding-patch.mjs +0 -193
- package/scripts/branding-patch.test.mjs +0 -89
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import assert from
|
|
2
|
-
import http from
|
|
3
|
-
import fs from
|
|
4
|
-
import os from
|
|
5
|
-
import path from
|
|
6
|
-
import { spawn } from
|
|
7
|
-
import test from
|
|
8
|
-
import {
|
|
9
|
-
buildPaddedLogoSnippet,
|
|
10
|
-
patchLogoSnippetText,
|
|
11
|
-
} from "./branding-patch.mjs";
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import test from 'node:test';
|
|
12
8
|
import {
|
|
13
9
|
buildOpenCodeConfig,
|
|
14
10
|
buildPackageLauncherInvocation,
|
|
@@ -17,205 +13,402 @@ import {
|
|
|
17
13
|
buildZenocodeBanner,
|
|
18
14
|
chooseDefaults,
|
|
19
15
|
hasLocalBaseUrlFlag,
|
|
20
|
-
|
|
16
|
+
isRecoverableSessionError,
|
|
17
|
+
resolveLaunchPackage,
|
|
18
|
+
resolvePackageRunners,
|
|
19
|
+
resolveConfiguredRuntimePackage,
|
|
21
20
|
resolveLoginBaseUrl,
|
|
22
21
|
resolveLoginSuccessIdentifier,
|
|
23
22
|
resolveTextCortexBaseUrl,
|
|
24
23
|
runRuntimeWithSessionRecovery,
|
|
25
24
|
shouldBypassZenocodePreparation,
|
|
26
25
|
shouldFallbackLoginToCloud,
|
|
26
|
+
zenocodeOwnedCommandMessage,
|
|
27
27
|
writePrivateJsonFile,
|
|
28
|
-
} from
|
|
28
|
+
} from './run-zenocode.mjs';
|
|
29
|
+
|
|
30
|
+
test('Zenocode launches the pinned TextCortex-owned runtime by default', () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
resolveLaunchPackage(),
|
|
33
|
+
'@textcortex/zenocode-ai@1.18.10-zenocode.1'
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('Zenocode ignores generic OpenCode package overrides', () => {
|
|
38
|
+
assert.equal(
|
|
39
|
+
resolveConfiguredRuntimePackage({ OPENCODE_PACKAGE: 'opencode-ai' }),
|
|
40
|
+
null
|
|
41
|
+
);
|
|
42
|
+
assert.equal(
|
|
43
|
+
resolveConfiguredRuntimePackage({
|
|
44
|
+
ZENOCODE_OPENCODE_PACKAGE: '@textcortex/zenocode-ai@dev',
|
|
45
|
+
}),
|
|
46
|
+
'@textcortex/zenocode-ai@dev'
|
|
47
|
+
);
|
|
48
|
+
});
|
|
29
49
|
|
|
30
|
-
test(
|
|
50
|
+
test('Zenocode owns runtime upgrades instead of forwarding them to OpenCode', () => {
|
|
51
|
+
assert.match(
|
|
52
|
+
zenocodeOwnedCommandMessage(['upgrade']),
|
|
53
|
+
/npm install -g @textcortex\/zenocode@latest/
|
|
54
|
+
);
|
|
55
|
+
assert.match(
|
|
56
|
+
zenocodeOwnedCommandMessage(['uninstall']),
|
|
57
|
+
/npm uninstall -g @textcortex\/zenocode/
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
zenocodeOwnedCommandMessage(['run', 'upgrade dependencies']),
|
|
61
|
+
null
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('chooseDefaults prefers GPT-6 Luna as the Zenocode default', () => {
|
|
31
66
|
const defaults = chooseDefaults({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
'gpt-6-sol': {},
|
|
68
|
+
'claude-sonnet-5-5': {},
|
|
69
|
+
'glm-5-3': {},
|
|
70
|
+
'kimi-k3-non-thinking': {},
|
|
71
|
+
'gpt-6-luna': {},
|
|
72
|
+
'gpt-5-6-luna': {},
|
|
36
73
|
});
|
|
37
74
|
|
|
38
75
|
assert.deepEqual(defaults, {
|
|
39
|
-
model:
|
|
40
|
-
smallModel:
|
|
76
|
+
model: 'gpt-6-luna',
|
|
77
|
+
smallModel: 'gpt-6-luna',
|
|
41
78
|
});
|
|
42
79
|
});
|
|
43
80
|
|
|
44
|
-
test(
|
|
45
|
-
assert.equal(packageNameFromSpecifier("opencode-ai@1.17.6"), "opencode-ai");
|
|
81
|
+
test('chooseDefaults ranks GLM 5.3 and Claude 5.5 above their predecessors', () => {
|
|
46
82
|
assert.equal(
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
chooseDefaults({ 'glm-5-2-non-thinking': {}, 'glm-5-3': {} }).model,
|
|
84
|
+
'glm-5-3'
|
|
49
85
|
);
|
|
50
86
|
assert.equal(
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
chooseDefaults({ 'claude-5-sonnet': {}, 'claude-sonnet-5-5': {} }).model,
|
|
88
|
+
'claude-sonnet-5-5'
|
|
53
89
|
);
|
|
54
90
|
});
|
|
55
91
|
|
|
56
|
-
test(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
test('chooseDefaults falls back to GPT-5.6 Luna when GPT-6 Luna is unavailable', () => {
|
|
93
|
+
const defaults = chooseDefaults({
|
|
94
|
+
'kimi-k2p7-code-non-thinking': {},
|
|
95
|
+
'gpt-5-6-luna': {},
|
|
96
|
+
'glm-5-2-non-thinking': {},
|
|
97
|
+
'minimax-m3-thinking': {},
|
|
98
|
+
'gpt-5-2': {},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(defaults, {
|
|
102
|
+
model: 'gpt-5-6-luna',
|
|
103
|
+
smallModel: 'gpt-5-6-luna',
|
|
104
|
+
});
|
|
62
105
|
});
|
|
63
106
|
|
|
64
|
-
test(
|
|
107
|
+
test('chooseDefaults falls back to Kimi K2.7 Code when GPT-5.6 Luna is unavailable', () => {
|
|
108
|
+
const defaults = chooseDefaults({
|
|
109
|
+
'kimi-k2p7-code-non-thinking': {},
|
|
110
|
+
'glm-5-2-non-thinking': {},
|
|
111
|
+
'glm-5-1-non-thinking': {},
|
|
112
|
+
'kimi-k2-6-non-thinking': {},
|
|
113
|
+
'minimax-m3-thinking': {},
|
|
114
|
+
'gpt-5-2': {},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.deepEqual(defaults, {
|
|
118
|
+
model: 'kimi-k2p7-code-non-thinking',
|
|
119
|
+
smallModel: 'kimi-k2p7-code-non-thinking',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('chooseDefaults never selects deprecated GLM 5.1 for Zenocode fallback', () => {
|
|
124
|
+
const defaults = chooseDefaults({
|
|
125
|
+
'glm-5-1-non-thinking': {},
|
|
126
|
+
'future-model': {},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(defaults, {
|
|
130
|
+
model: 'future-model',
|
|
131
|
+
smallModel: 'future-model',
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('chooseDefaults falls back to explicit thinking variants when base models are absent', () => {
|
|
136
|
+
const defaults = chooseDefaults({
|
|
137
|
+
'kimi-k2p7-code-thinking': {},
|
|
138
|
+
'glm-5-2-non-thinking': {},
|
|
139
|
+
'kimi-k2-6-thinking': {},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.deepEqual(defaults, {
|
|
143
|
+
model: 'kimi-k2p7-code-thinking',
|
|
144
|
+
smallModel: 'kimi-k2p7-code-thinking',
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('chooseDefaults prefers Kimi K3 over older Kimi generations', () => {
|
|
149
|
+
const defaults = chooseDefaults({
|
|
150
|
+
'kimi-k2p7-code-non-thinking': {},
|
|
151
|
+
'kimi-k3-non-thinking': {},
|
|
152
|
+
'kimi-k2-6-non-thinking': {},
|
|
153
|
+
'minimax-m3-thinking': {},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.deepEqual(defaults, {
|
|
157
|
+
model: 'kimi-k3-non-thinking',
|
|
158
|
+
smallModel: 'kimi-k3-non-thinking',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('chooseDefaults ranks Claude 5 below the coding-first models', () => {
|
|
163
|
+
const defaults = chooseDefaults({
|
|
164
|
+
'claude-opus-5': {},
|
|
165
|
+
'claude-5-sonnet': {},
|
|
166
|
+
'kimi-k3-thinking': {},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
assert.deepEqual(defaults, {
|
|
170
|
+
model: 'kimi-k3-thinking',
|
|
171
|
+
smallModel: 'kimi-k3-thinking',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('chooseDefaults selects Claude 5 when it is the only catalog entry', () => {
|
|
176
|
+
const defaults = chooseDefaults({
|
|
177
|
+
'claude-5-sonnet': {},
|
|
178
|
+
'claude-opus-5': {},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
assert.deepEqual(defaults, {
|
|
182
|
+
model: 'claude-5-sonnet',
|
|
183
|
+
smallModel: 'claude-5-sonnet',
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('shouldBypassZenocodePreparation skips auth for runtime metadata commands', () => {
|
|
188
|
+
assert.equal(shouldBypassZenocodePreparation(['--version']), true);
|
|
189
|
+
assert.equal(shouldBypassZenocodePreparation(['completion', 'zsh']), true);
|
|
190
|
+
assert.equal(shouldBypassZenocodePreparation(['run']), false);
|
|
191
|
+
assert.equal(
|
|
192
|
+
shouldBypassZenocodePreparation(['run', 'implement', 'completion']),
|
|
193
|
+
false
|
|
194
|
+
);
|
|
195
|
+
assert.equal(shouldBypassZenocodePreparation(['run', '--help']), false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('resolveTextCortexBaseUrl defaults to the cloud API for packaged usage', () => {
|
|
65
199
|
assert.equal(
|
|
66
200
|
resolveTextCortexBaseUrl({
|
|
67
|
-
envBaseUrl:
|
|
201
|
+
envBaseUrl: '',
|
|
68
202
|
storedBaseUrl: null,
|
|
69
203
|
}),
|
|
70
|
-
|
|
204
|
+
'https://api.textcortex.com'
|
|
71
205
|
);
|
|
72
206
|
});
|
|
73
207
|
|
|
74
|
-
test(
|
|
208
|
+
test('resolveTextCortexBaseUrl prefers the explicit env var over stored credentials', () => {
|
|
75
209
|
assert.equal(
|
|
76
210
|
resolveTextCortexBaseUrl({
|
|
77
|
-
envBaseUrl:
|
|
78
|
-
storedBaseUrl:
|
|
211
|
+
envBaseUrl: 'https://staging.textcortex.com',
|
|
212
|
+
storedBaseUrl: 'http://127.0.0.1:8080',
|
|
79
213
|
}),
|
|
80
|
-
|
|
214
|
+
'https://staging.textcortex.com'
|
|
81
215
|
);
|
|
82
216
|
});
|
|
83
217
|
|
|
84
|
-
test(
|
|
218
|
+
test('resolveTextCortexBaseUrl prefers localhost when the local flag is enabled', () => {
|
|
85
219
|
assert.equal(
|
|
86
220
|
resolveTextCortexBaseUrl({
|
|
87
|
-
envBaseUrl:
|
|
88
|
-
storedBaseUrl:
|
|
221
|
+
envBaseUrl: 'https://staging.textcortex.com',
|
|
222
|
+
storedBaseUrl: 'https://api.textcortex.com',
|
|
89
223
|
preferLocalhost: true,
|
|
90
224
|
}),
|
|
91
|
-
|
|
225
|
+
'http://127.0.0.1:8080'
|
|
92
226
|
);
|
|
93
227
|
});
|
|
94
228
|
|
|
95
|
-
test(
|
|
229
|
+
test('resolveLoginBaseUrl ignores stored credentials and defaults explicit login to cloud', () => {
|
|
96
230
|
assert.equal(
|
|
97
231
|
resolveLoginBaseUrl({
|
|
98
|
-
envBaseUrl:
|
|
232
|
+
envBaseUrl: '',
|
|
99
233
|
}),
|
|
100
|
-
|
|
234
|
+
'https://api.textcortex.com'
|
|
101
235
|
);
|
|
102
236
|
});
|
|
103
237
|
|
|
104
|
-
test(
|
|
238
|
+
test('resolveLoginBaseUrl still respects explicit env overrides', () => {
|
|
105
239
|
assert.equal(
|
|
106
240
|
resolveLoginBaseUrl({
|
|
107
|
-
envBaseUrl:
|
|
241
|
+
envBaseUrl: 'https://staging.textcortex.com',
|
|
108
242
|
}),
|
|
109
|
-
|
|
243
|
+
'https://staging.textcortex.com'
|
|
110
244
|
);
|
|
111
245
|
});
|
|
112
246
|
|
|
113
|
-
test(
|
|
247
|
+
test('resolveLoginBaseUrl prefers localhost when the local flag is enabled', () => {
|
|
114
248
|
assert.equal(
|
|
115
249
|
resolveLoginBaseUrl({
|
|
116
|
-
envBaseUrl:
|
|
250
|
+
envBaseUrl: 'https://staging.textcortex.com',
|
|
117
251
|
preferLocalhost: true,
|
|
118
252
|
}),
|
|
119
|
-
|
|
253
|
+
'http://127.0.0.1:8080'
|
|
120
254
|
);
|
|
121
255
|
});
|
|
122
256
|
|
|
123
|
-
test(
|
|
124
|
-
assert.equal(hasLocalBaseUrlFlag([
|
|
125
|
-
assert.equal(hasLocalBaseUrlFlag([
|
|
126
|
-
assert.equal(hasLocalBaseUrlFlag([
|
|
257
|
+
test('hasLocalBaseUrlFlag detects localhost flags', () => {
|
|
258
|
+
assert.equal(hasLocalBaseUrlFlag(['login', '--local']), true);
|
|
259
|
+
assert.equal(hasLocalBaseUrlFlag(['--localhost', 'run']), true);
|
|
260
|
+
assert.equal(hasLocalBaseUrlFlag(['run', '--help']), false);
|
|
127
261
|
});
|
|
128
262
|
|
|
129
|
-
test(
|
|
263
|
+
test('shouldFallbackLoginToCloud retries the cloud API when the local auth route returns 404', () => {
|
|
130
264
|
assert.equal(
|
|
131
265
|
shouldFallbackLoginToCloud({
|
|
132
|
-
baseUrl:
|
|
266
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
133
267
|
hasExplicitBaseUrl: false,
|
|
134
|
-
error: new Error(
|
|
268
|
+
error: new Error('Login initiate failed (404): not found'),
|
|
135
269
|
}),
|
|
136
|
-
true
|
|
270
|
+
true
|
|
137
271
|
);
|
|
138
272
|
});
|
|
139
273
|
|
|
140
|
-
test(
|
|
274
|
+
test('shouldFallbackLoginToCloud does not override an explicit base URL', () => {
|
|
141
275
|
assert.equal(
|
|
142
276
|
shouldFallbackLoginToCloud({
|
|
143
|
-
baseUrl:
|
|
277
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
144
278
|
hasExplicitBaseUrl: true,
|
|
145
|
-
error: new Error(
|
|
279
|
+
error: new Error('fetch failed'),
|
|
146
280
|
}),
|
|
147
|
-
false
|
|
281
|
+
false
|
|
148
282
|
);
|
|
149
283
|
});
|
|
150
284
|
|
|
151
|
-
test(
|
|
285
|
+
test('buildOpenCodeConfig includes an openai stub for fallback runtime auth plugins', () => {
|
|
152
286
|
const config = buildOpenCodeConfig({
|
|
153
|
-
baseUrl:
|
|
154
|
-
providerID:
|
|
155
|
-
model:
|
|
156
|
-
smallModel:
|
|
287
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
288
|
+
providerID: 'textcortex',
|
|
289
|
+
model: 'minimax-m3-thinking',
|
|
290
|
+
smallModel: 'gpt-5-2',
|
|
157
291
|
});
|
|
158
292
|
|
|
159
|
-
assert.deepEqual(config.enabled_providers, [
|
|
160
|
-
assert.equal(config.model,
|
|
161
|
-
assert.equal(config.small_model,
|
|
162
|
-
assert.equal(config.theme,
|
|
293
|
+
assert.deepEqual(config.enabled_providers, ['textcortex']);
|
|
294
|
+
assert.equal(config.model, 'textcortex/minimax-m3-thinking');
|
|
295
|
+
assert.equal(config.small_model, 'textcortex/gpt-5-2');
|
|
296
|
+
assert.equal(config.theme, 'system');
|
|
297
|
+
assert.equal(config.autoupdate, false);
|
|
163
298
|
assert.ok(config.provider.openai);
|
|
164
299
|
assert.deepEqual(config.provider.openai.models, {});
|
|
165
300
|
});
|
|
166
301
|
|
|
167
|
-
test(
|
|
168
|
-
const childOptions = buildPackageLauncherChildOptions(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
},
|
|
173
|
-
"/tmp/zenocode-runtime",
|
|
174
|
-
);
|
|
302
|
+
test('buildPackageLauncherChildOptions keeps fallback package launchers attached to the terminal', () => {
|
|
303
|
+
const childOptions = buildPackageLauncherChildOptions({
|
|
304
|
+
cwd: '/tmp/zenocode',
|
|
305
|
+
env: { TEXTCORTEX_API_KEY: 'token-1' },
|
|
306
|
+
});
|
|
175
307
|
|
|
176
|
-
assert.equal(childOptions.stdio,
|
|
177
|
-
assert.equal(childOptions.cwd,
|
|
308
|
+
assert.equal(childOptions.stdio, 'inherit');
|
|
309
|
+
assert.equal(childOptions.cwd, '/tmp/zenocode');
|
|
178
310
|
assert.deepEqual(childOptions.env, {
|
|
179
|
-
TEXTCORTEX_API_KEY:
|
|
180
|
-
OPENCODE_BIN_PATH: "/tmp/zenocode-runtime",
|
|
311
|
+
TEXTCORTEX_API_KEY: 'token-1',
|
|
181
312
|
});
|
|
182
313
|
});
|
|
183
314
|
|
|
184
|
-
test(
|
|
315
|
+
test('buildPackageLauncherInvocation launches the owned runtime package unchanged', () => {
|
|
185
316
|
const invocation = buildPackageLauncherInvocation({
|
|
186
317
|
runner: {
|
|
187
|
-
command:
|
|
188
|
-
args: [
|
|
318
|
+
command: 'pnpm',
|
|
319
|
+
args: ['dlx', '@textcortex/zenocode-ai@1.18.10-zenocode.1', '--version'],
|
|
189
320
|
},
|
|
190
|
-
args: ["--version"],
|
|
191
|
-
pinnedRuntimePath: "/tmp/zenocode-runtime",
|
|
192
321
|
});
|
|
193
322
|
|
|
194
323
|
assert.deepEqual(invocation, {
|
|
195
|
-
command:
|
|
196
|
-
args: [
|
|
197
|
-
usePinnedRuntime: true,
|
|
324
|
+
command: 'pnpm',
|
|
325
|
+
args: ['dlx', '@textcortex/zenocode-ai@1.18.10-zenocode.1', '--version'],
|
|
198
326
|
});
|
|
199
327
|
});
|
|
200
328
|
|
|
201
|
-
test(
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
329
|
+
test('resolvePackageRunners keeps pnpm then npx on POSIX platforms', () => {
|
|
330
|
+
const runners = resolvePackageRunners(
|
|
331
|
+
'@textcortex/zenocode-ai@1.0.0',
|
|
332
|
+
['run'],
|
|
333
|
+
{
|
|
334
|
+
platform: 'linux',
|
|
335
|
+
execPath: '/usr/bin/node',
|
|
336
|
+
env: {},
|
|
337
|
+
fileExists: () => true,
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
assert.deepEqual(runners, [
|
|
342
|
+
{ command: 'pnpm', args: ['dlx', '@textcortex/zenocode-ai@1.0.0', 'run'] },
|
|
343
|
+
{ command: 'npx', args: ['--yes', '@textcortex/zenocode-ai@1.0.0', 'run'] },
|
|
344
|
+
]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('resolvePackageRunners never spawns .cmd/.bat shims on Windows', () => {
|
|
348
|
+
// Node >= 18.20.2 / 20.12.2 rejects spawning batch files without a shell
|
|
349
|
+
// (CVE-2024-27980) with `spawn EINVAL`, so Windows runners must be executables.
|
|
350
|
+
const execPath = 'C:\\Program Files\\nodejs\\node.exe';
|
|
351
|
+
const npxCli = path.win32.join(
|
|
352
|
+
'C:\\Program Files\\nodejs',
|
|
353
|
+
'node_modules',
|
|
354
|
+
'npm',
|
|
355
|
+
'bin',
|
|
356
|
+
'npx-cli.js'
|
|
357
|
+
);
|
|
358
|
+
const runners = resolvePackageRunners(
|
|
359
|
+
'@textcortex/zenocode-ai@1.0.0',
|
|
360
|
+
['run'],
|
|
361
|
+
{
|
|
362
|
+
platform: 'win32',
|
|
363
|
+
execPath,
|
|
364
|
+
env: {},
|
|
365
|
+
fileExists: (candidate) => candidate === npxCli,
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
for (const runner of runners) {
|
|
370
|
+
assert.doesNotMatch(runner.command, /\.(cmd|bat)$/i);
|
|
371
|
+
}
|
|
372
|
+
assert.deepEqual(runners, [
|
|
373
|
+
{
|
|
374
|
+
command: 'pnpm.exe',
|
|
375
|
+
args: ['dlx', '@textcortex/zenocode-ai@1.0.0', 'run'],
|
|
206
376
|
},
|
|
207
|
-
|
|
208
|
-
|
|
377
|
+
{
|
|
378
|
+
command: execPath,
|
|
379
|
+
args: [npxCli, '--yes', '@textcortex/zenocode-ai@1.0.0', 'run'],
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('resolvePackageRunners finds npx beside npm_execpath on Windows', () => {
|
|
385
|
+
const npmCli = 'D:\\tools\\npm\\bin\\npm-cli.js';
|
|
386
|
+
const npxCli = 'D:\\tools\\npm\\bin\\npx-cli.js';
|
|
387
|
+
const runners = resolvePackageRunners('pkg@1', [], {
|
|
388
|
+
platform: 'win32',
|
|
389
|
+
execPath: 'D:\\volta\\node.exe',
|
|
390
|
+
env: { npm_execpath: npmCli },
|
|
391
|
+
fileExists: (candidate) => candidate === npxCli,
|
|
209
392
|
});
|
|
210
393
|
|
|
211
|
-
assert.deepEqual(
|
|
212
|
-
command:
|
|
213
|
-
args: [
|
|
214
|
-
usePinnedRuntime: false,
|
|
394
|
+
assert.deepEqual(runners.at(-1), {
|
|
395
|
+
command: 'D:\\volta\\node.exe',
|
|
396
|
+
args: [npxCli, '--yes', 'pkg@1'],
|
|
215
397
|
});
|
|
216
398
|
});
|
|
217
399
|
|
|
218
|
-
test(
|
|
400
|
+
test('resolvePackageRunners omits npx on Windows when npm is not beside node', () => {
|
|
401
|
+
const runners = resolvePackageRunners('pkg@1', [], {
|
|
402
|
+
platform: 'win32',
|
|
403
|
+
execPath: 'C:\\node\\node.exe',
|
|
404
|
+
env: {},
|
|
405
|
+
fileExists: () => false,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
assert.deepEqual(runners, [{ command: 'pnpm.exe', args: ['dlx', 'pkg@1'] }]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('buildZenocodeBanner renders block logo art instead of plain text', () => {
|
|
219
412
|
const banner = buildZenocodeBanner();
|
|
220
413
|
|
|
221
414
|
assert.match(banner, /▀▀█/);
|
|
@@ -224,51 +417,20 @@ test("buildZenocodeBanner renders block logo art instead of plain text", () => {
|
|
|
224
417
|
assert.doesNotMatch(banner, /\bZenocode\b/);
|
|
225
418
|
});
|
|
226
419
|
|
|
227
|
-
test(
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
right: [" ", " ", " ", " "]
|
|
231
|
-
};
|
|
232
|
-
var marks = "_^~";`;
|
|
233
|
-
|
|
234
|
-
const result = patchLogoSnippetText(buildPaddedLogoSnippet(legacyTextLogoSnippet));
|
|
235
|
-
|
|
236
|
-
assert.equal(result.patched, true);
|
|
237
|
-
assert.match(result.text, /\\u2580\\u2580\\u2588/);
|
|
238
|
-
assert.match(result.text, /\\u2588\\u2580\\u2580/);
|
|
239
|
-
assert.doesNotMatch(result.text, /\\\\u2580/);
|
|
240
|
-
assert.doesNotMatch(result.text, /Zenocode/);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("patchLogoSnippetText upgrades double-escaped Zenocode glyph logos", () => {
|
|
244
|
-
const brokenGlyphLogoSnippet = String.raw`var logo = {
|
|
245
|
-
left: [" "," \\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580 \\u2588\\u2584 \\u2588 \\u2588\\u2580\\u2588"," \\u2588\\u2584\\u2584 \\u2588\\u2588\\u2584 \\u2588 \\u2580\\u2588 \\u2588\\u2584\\u2588"," "],
|
|
246
|
-
right: [" "," \\u2588\\u2580\\u2580 \\u2588\\u2580\\u2588 \\u2588\\u2580\\u2584 \\u2588\\u2580\\u2580"," \\u2588\\u2584\\u2584 \\u2588\\u2584\\u2588 \\u2588\\u2584\\u2580 \\u2588\\u2588\\u2584"," "]
|
|
247
|
-
};
|
|
248
|
-
var marks = "_^~";`;
|
|
249
|
-
|
|
250
|
-
const result = patchLogoSnippetText(buildPaddedLogoSnippet(brokenGlyphLogoSnippet));
|
|
251
|
-
|
|
252
|
-
assert.equal(result.patched, true);
|
|
253
|
-
assert.match(result.text, /\\u2580\\u2580\\u2588/);
|
|
254
|
-
assert.match(result.text, /\\u2588\\u2580\\u2580/);
|
|
255
|
-
assert.doesNotMatch(result.text, /\\\\u2580/);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("writePrivateJsonFile stores credentials with private permissions", async (t) => {
|
|
259
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-runtime-"));
|
|
260
|
-
const credentialsPath = path.join(tempDir, "runtime", "credentials.json");
|
|
420
|
+
test('writePrivateJsonFile stores credentials with private permissions', async (t) => {
|
|
421
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'zenocode-runtime-'));
|
|
422
|
+
const credentialsPath = path.join(tempDir, 'runtime', 'credentials.json');
|
|
261
423
|
|
|
262
424
|
t.after(async () => {
|
|
263
425
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
264
426
|
});
|
|
265
427
|
|
|
266
|
-
await writePrivateJsonFile(credentialsPath, { access_token:
|
|
428
|
+
await writePrivateJsonFile(credentialsPath, { access_token: 'secret-token' });
|
|
267
429
|
|
|
268
|
-
const saved = JSON.parse(await fs.readFile(credentialsPath,
|
|
269
|
-
assert.equal(saved.access_token,
|
|
430
|
+
const saved = JSON.parse(await fs.readFile(credentialsPath, 'utf-8'));
|
|
431
|
+
assert.equal(saved.access_token, 'secret-token');
|
|
270
432
|
|
|
271
|
-
if (process.platform !==
|
|
433
|
+
if (process.platform !== 'win32') {
|
|
272
434
|
const dirMode = (await fs.stat(path.dirname(credentialsPath))).mode & 0o777;
|
|
273
435
|
const fileMode = (await fs.stat(credentialsPath)).mode & 0o777;
|
|
274
436
|
|
|
@@ -277,68 +439,89 @@ test("writePrivateJsonFile stores credentials with private permissions", async (
|
|
|
277
439
|
}
|
|
278
440
|
});
|
|
279
441
|
|
|
280
|
-
test(
|
|
442
|
+
test('canRecoverRuntimeSessionFromTranscript detects expired session output', () => {
|
|
281
443
|
const transcript = [
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
444
|
+
'\u001b[2J\u001b[H',
|
|
445
|
+
'cool, is there anything about zenocode?',
|
|
446
|
+
'',
|
|
285
447
|
'Unauthorized: {"detail":"Token has expired"}',
|
|
286
|
-
].join(
|
|
448
|
+
].join('\n');
|
|
287
449
|
|
|
288
450
|
assert.equal(canRecoverRuntimeSessionFromTranscript(transcript), true);
|
|
289
451
|
});
|
|
290
452
|
|
|
291
|
-
test(
|
|
453
|
+
test('isRecoverableSessionError accepts expired and malformed model tokens', () => {
|
|
454
|
+
assert.equal(
|
|
455
|
+
isRecoverableSessionError(
|
|
456
|
+
new Error('Models fetch failed (401): Token has expired')
|
|
457
|
+
),
|
|
458
|
+
true
|
|
459
|
+
);
|
|
460
|
+
assert.equal(
|
|
461
|
+
isRecoverableSessionError(
|
|
462
|
+
new Error('Models fetch failed (401): Invalid token format')
|
|
463
|
+
),
|
|
464
|
+
true
|
|
465
|
+
);
|
|
466
|
+
assert.equal(
|
|
467
|
+
isRecoverableSessionError(
|
|
468
|
+
new Error('Models fetch failed (401): forbidden')
|
|
469
|
+
),
|
|
470
|
+
false
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('resolveLoginSuccessIdentifier avoids email fallback', () => {
|
|
292
475
|
assert.equal(
|
|
293
476
|
resolveLoginSuccessIdentifier({
|
|
294
|
-
auth_id:
|
|
295
|
-
email:
|
|
477
|
+
auth_id: 'auth_123',
|
|
478
|
+
email: 'user@example.com',
|
|
296
479
|
}),
|
|
297
|
-
|
|
480
|
+
'auth_123'
|
|
298
481
|
);
|
|
299
482
|
assert.equal(
|
|
300
483
|
resolveLoginSuccessIdentifier({
|
|
301
|
-
email:
|
|
484
|
+
email: 'user@example.com',
|
|
302
485
|
}),
|
|
303
|
-
|
|
486
|
+
'unknown'
|
|
304
487
|
);
|
|
305
488
|
});
|
|
306
489
|
|
|
307
|
-
test(
|
|
490
|
+
test('runRuntimeWithSessionRecovery reruns login after runtime session expiry', async () => {
|
|
308
491
|
const events = [];
|
|
309
492
|
let launchCount = 0;
|
|
310
493
|
|
|
311
494
|
const result = await runRuntimeWithSessionRecovery({
|
|
312
|
-
args: [
|
|
313
|
-
baseUrl:
|
|
314
|
-
token:
|
|
495
|
+
args: ['run'],
|
|
496
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
497
|
+
token: 'token-1',
|
|
315
498
|
childOptions: {
|
|
316
499
|
cwd: process.cwd(),
|
|
317
500
|
env: {},
|
|
318
501
|
},
|
|
319
502
|
canAutoLoginRuntime: true,
|
|
320
503
|
refreshTokenFn: async (baseUrl) => {
|
|
321
|
-
events.push([
|
|
322
|
-
throw new Error(
|
|
504
|
+
events.push(['refresh', baseUrl]);
|
|
505
|
+
throw new Error('refresh failed');
|
|
323
506
|
},
|
|
324
507
|
runLogin: async (baseUrl, loginArgs) => {
|
|
325
|
-
events.push([
|
|
508
|
+
events.push(['login', baseUrl, loginArgs]);
|
|
326
509
|
},
|
|
327
510
|
resolveTokenFn: async () => {
|
|
328
|
-
events.push([
|
|
329
|
-
return
|
|
511
|
+
events.push(['resolve-token']);
|
|
512
|
+
return 'token-2';
|
|
330
513
|
},
|
|
331
514
|
resolveStoredBaseUrlFn: async () => {
|
|
332
|
-
events.push([
|
|
333
|
-
return
|
|
515
|
+
events.push(['resolve-base-url']);
|
|
516
|
+
return 'https://api.textcortex.com';
|
|
334
517
|
},
|
|
335
518
|
prepareRuntimeFn: async (baseUrl, token) => {
|
|
336
|
-
events.push([
|
|
337
|
-
return
|
|
519
|
+
events.push(['prepare', baseUrl, token]);
|
|
520
|
+
return 'kimi-k2-5-thinking';
|
|
338
521
|
},
|
|
339
522
|
launchRuntimeFn: async ({ childOptions }) => {
|
|
340
523
|
launchCount += 1;
|
|
341
|
-
events.push([
|
|
524
|
+
events.push(['launch', launchCount, childOptions.env.TEXTCORTEX_API_KEY]);
|
|
342
525
|
if (launchCount === 1) {
|
|
343
526
|
return { expiredSession: true };
|
|
344
527
|
}
|
|
@@ -347,47 +530,47 @@ test("runRuntimeWithSessionRecovery reruns login after runtime session expiry",
|
|
|
347
530
|
});
|
|
348
531
|
|
|
349
532
|
assert.deepEqual(events, [
|
|
350
|
-
[
|
|
351
|
-
[
|
|
352
|
-
[
|
|
353
|
-
[
|
|
354
|
-
[
|
|
355
|
-
[
|
|
356
|
-
[
|
|
533
|
+
['launch', 1, 'token-1'],
|
|
534
|
+
['refresh', 'http://127.0.0.1:8080'],
|
|
535
|
+
['login', 'http://127.0.0.1:8080', []],
|
|
536
|
+
['resolve-token'],
|
|
537
|
+
['resolve-base-url'],
|
|
538
|
+
['prepare', 'https://api.textcortex.com', 'token-2'],
|
|
539
|
+
['launch', 2, 'token-2'],
|
|
357
540
|
]);
|
|
358
541
|
assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
|
|
359
542
|
});
|
|
360
543
|
|
|
361
|
-
test(
|
|
544
|
+
test('runRuntimeWithSessionRecovery refreshes stored credentials before forcing login', async () => {
|
|
362
545
|
const events = [];
|
|
363
546
|
let launchCount = 0;
|
|
364
547
|
|
|
365
548
|
const result = await runRuntimeWithSessionRecovery({
|
|
366
|
-
args: [
|
|
367
|
-
baseUrl:
|
|
368
|
-
token:
|
|
549
|
+
args: ['run'],
|
|
550
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
551
|
+
token: 'token-1',
|
|
369
552
|
childOptions: {
|
|
370
553
|
cwd: process.cwd(),
|
|
371
554
|
env: {},
|
|
372
555
|
},
|
|
373
556
|
canAutoLoginRuntime: true,
|
|
374
557
|
refreshTokenFn: async (baseUrl) => {
|
|
375
|
-
events.push([
|
|
558
|
+
events.push(['refresh', baseUrl]);
|
|
376
559
|
return {
|
|
377
|
-
token:
|
|
378
|
-
baseUrl:
|
|
560
|
+
token: 'token-2',
|
|
561
|
+
baseUrl: 'https://api.textcortex.com',
|
|
379
562
|
};
|
|
380
563
|
},
|
|
381
564
|
runLogin: async () => {
|
|
382
|
-
throw new Error(
|
|
565
|
+
throw new Error('login should not be called');
|
|
383
566
|
},
|
|
384
567
|
prepareRuntimeFn: async (baseUrl, token) => {
|
|
385
|
-
events.push([
|
|
386
|
-
return
|
|
568
|
+
events.push(['prepare', baseUrl, token]);
|
|
569
|
+
return 'kimi-k2-5-thinking';
|
|
387
570
|
},
|
|
388
571
|
launchRuntimeFn: async ({ childOptions }) => {
|
|
389
572
|
launchCount += 1;
|
|
390
|
-
events.push([
|
|
573
|
+
events.push(['launch', launchCount, childOptions.env.TEXTCORTEX_API_KEY]);
|
|
391
574
|
if (launchCount === 1) {
|
|
392
575
|
return { expiredSession: true };
|
|
393
576
|
}
|
|
@@ -396,22 +579,22 @@ test("runRuntimeWithSessionRecovery refreshes stored credentials before forcing
|
|
|
396
579
|
});
|
|
397
580
|
|
|
398
581
|
assert.deepEqual(events, [
|
|
399
|
-
[
|
|
400
|
-
[
|
|
401
|
-
[
|
|
402
|
-
[
|
|
582
|
+
['launch', 1, 'token-1'],
|
|
583
|
+
['refresh', 'http://127.0.0.1:8080'],
|
|
584
|
+
['prepare', 'https://api.textcortex.com', 'token-2'],
|
|
585
|
+
['launch', 2, 'token-2'],
|
|
403
586
|
]);
|
|
404
587
|
assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
|
|
405
588
|
});
|
|
406
589
|
|
|
407
|
-
test(
|
|
590
|
+
test('runRuntimeWithSessionRecovery preserves explicit localhost preference during login recovery', async () => {
|
|
408
591
|
const events = [];
|
|
409
592
|
let launchCount = 0;
|
|
410
593
|
|
|
411
594
|
const result = await runRuntimeWithSessionRecovery({
|
|
412
|
-
args: [
|
|
413
|
-
baseUrl:
|
|
414
|
-
token:
|
|
595
|
+
args: ['run'],
|
|
596
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
597
|
+
token: 'token-1',
|
|
415
598
|
childOptions: {
|
|
416
599
|
cwd: process.cwd(),
|
|
417
600
|
env: {},
|
|
@@ -419,27 +602,27 @@ test("runRuntimeWithSessionRecovery preserves explicit localhost preference duri
|
|
|
419
602
|
canAutoLoginRuntime: true,
|
|
420
603
|
preferLocalhost: true,
|
|
421
604
|
refreshTokenFn: async (baseUrl) => {
|
|
422
|
-
events.push([
|
|
423
|
-
throw new Error(
|
|
605
|
+
events.push(['refresh', baseUrl]);
|
|
606
|
+
throw new Error('refresh failed');
|
|
424
607
|
},
|
|
425
608
|
runLogin: async (baseUrl, loginArgs, options) => {
|
|
426
|
-
events.push([
|
|
609
|
+
events.push(['login', baseUrl, loginArgs, options]);
|
|
427
610
|
},
|
|
428
611
|
resolveTokenFn: async () => {
|
|
429
|
-
events.push([
|
|
430
|
-
return
|
|
612
|
+
events.push(['resolve-token']);
|
|
613
|
+
return 'token-2';
|
|
431
614
|
},
|
|
432
615
|
resolveStoredBaseUrlFn: async () => {
|
|
433
|
-
events.push([
|
|
434
|
-
return
|
|
616
|
+
events.push(['resolve-base-url']);
|
|
617
|
+
return 'https://api.textcortex.com';
|
|
435
618
|
},
|
|
436
619
|
prepareRuntimeFn: async (baseUrl, token) => {
|
|
437
|
-
events.push([
|
|
438
|
-
return
|
|
620
|
+
events.push(['prepare', baseUrl, token]);
|
|
621
|
+
return 'kimi-k2-5-thinking';
|
|
439
622
|
},
|
|
440
623
|
launchRuntimeFn: async ({ childOptions }) => {
|
|
441
624
|
launchCount += 1;
|
|
442
|
-
events.push([
|
|
625
|
+
events.push(['launch', launchCount, childOptions.env.TEXTCORTEX_API_KEY]);
|
|
443
626
|
if (launchCount === 1) {
|
|
444
627
|
return { expiredSession: true };
|
|
445
628
|
}
|
|
@@ -448,27 +631,27 @@ test("runRuntimeWithSessionRecovery preserves explicit localhost preference duri
|
|
|
448
631
|
});
|
|
449
632
|
|
|
450
633
|
assert.deepEqual(events, [
|
|
451
|
-
[
|
|
452
|
-
[
|
|
453
|
-
[
|
|
454
|
-
[
|
|
455
|
-
[
|
|
456
|
-
[
|
|
457
|
-
[
|
|
634
|
+
['launch', 1, 'token-1'],
|
|
635
|
+
['refresh', 'http://127.0.0.1:8080'],
|
|
636
|
+
['login', 'http://127.0.0.1:8080', ['--local'], { preferLocalhost: true }],
|
|
637
|
+
['resolve-token'],
|
|
638
|
+
['resolve-base-url'],
|
|
639
|
+
['prepare', 'http://127.0.0.1:8080', 'token-2'],
|
|
640
|
+
['launch', 2, 'token-2'],
|
|
458
641
|
]);
|
|
459
642
|
assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
|
|
460
643
|
});
|
|
461
644
|
|
|
462
|
-
test(
|
|
645
|
+
test('runRuntimeWithSessionRecovery stops after repeated recovery failures', async () => {
|
|
463
646
|
const events = [];
|
|
464
647
|
let launchCount = 0;
|
|
465
648
|
|
|
466
649
|
await assert.rejects(
|
|
467
650
|
() =>
|
|
468
651
|
runRuntimeWithSessionRecovery({
|
|
469
|
-
args: [
|
|
470
|
-
baseUrl:
|
|
471
|
-
token:
|
|
652
|
+
args: ['run'],
|
|
653
|
+
baseUrl: 'http://127.0.0.1:8080',
|
|
654
|
+
token: 'token-1',
|
|
472
655
|
childOptions: {
|
|
473
656
|
cwd: process.cwd(),
|
|
474
657
|
env: {},
|
|
@@ -476,60 +659,64 @@ test("runRuntimeWithSessionRecovery stops after repeated recovery failures", asy
|
|
|
476
659
|
canAutoLoginRuntime: true,
|
|
477
660
|
maxRecoveryAttempts: 2,
|
|
478
661
|
sleepFn: async () => {
|
|
479
|
-
events.push([
|
|
662
|
+
events.push(['sleep']);
|
|
480
663
|
},
|
|
481
664
|
refreshTokenFn: async (baseUrl) => {
|
|
482
|
-
events.push([
|
|
483
|
-
throw new Error(
|
|
665
|
+
events.push(['refresh', baseUrl]);
|
|
666
|
+
throw new Error('refresh failed');
|
|
484
667
|
},
|
|
485
668
|
runLogin: async (baseUrl, loginArgs) => {
|
|
486
|
-
events.push([
|
|
669
|
+
events.push(['login', baseUrl, loginArgs]);
|
|
487
670
|
},
|
|
488
671
|
resolveTokenFn: async () => {
|
|
489
|
-
events.push([
|
|
490
|
-
return
|
|
672
|
+
events.push(['resolve-token']);
|
|
673
|
+
return 'token-2';
|
|
491
674
|
},
|
|
492
675
|
resolveStoredBaseUrlFn: async () => {
|
|
493
|
-
events.push([
|
|
494
|
-
return
|
|
676
|
+
events.push(['resolve-base-url']);
|
|
677
|
+
return 'https://api.textcortex.com';
|
|
495
678
|
},
|
|
496
679
|
prepareRuntimeFn: async (baseUrl, token) => {
|
|
497
|
-
events.push([
|
|
498
|
-
return
|
|
680
|
+
events.push(['prepare', baseUrl, token]);
|
|
681
|
+
return 'kimi-k2-5-thinking';
|
|
499
682
|
},
|
|
500
683
|
launchRuntimeFn: async ({ childOptions }) => {
|
|
501
684
|
launchCount += 1;
|
|
502
|
-
events.push([
|
|
685
|
+
events.push([
|
|
686
|
+
'launch',
|
|
687
|
+
launchCount,
|
|
688
|
+
childOptions.env.TEXTCORTEX_API_KEY,
|
|
689
|
+
]);
|
|
503
690
|
return { expiredSession: true };
|
|
504
691
|
},
|
|
505
692
|
}),
|
|
506
|
-
/Zenocode session recovery failed after 2 attempts
|
|
693
|
+
/Zenocode session recovery failed after 2 attempts/
|
|
507
694
|
);
|
|
508
695
|
|
|
509
696
|
assert.deepEqual(events, [
|
|
510
|
-
[
|
|
511
|
-
[
|
|
512
|
-
[
|
|
513
|
-
[
|
|
514
|
-
[
|
|
515
|
-
[
|
|
516
|
-
[
|
|
517
|
-
[
|
|
518
|
-
[
|
|
519
|
-
[
|
|
520
|
-
[
|
|
521
|
-
[
|
|
522
|
-
[
|
|
523
|
-
[
|
|
524
|
-
[
|
|
697
|
+
['launch', 1, 'token-1'],
|
|
698
|
+
['refresh', 'http://127.0.0.1:8080'],
|
|
699
|
+
['login', 'http://127.0.0.1:8080', []],
|
|
700
|
+
['resolve-token'],
|
|
701
|
+
['resolve-base-url'],
|
|
702
|
+
['prepare', 'https://api.textcortex.com', 'token-2'],
|
|
703
|
+
['sleep'],
|
|
704
|
+
['launch', 2, 'token-2'],
|
|
705
|
+
['refresh', 'https://api.textcortex.com'],
|
|
706
|
+
['login', 'https://api.textcortex.com', []],
|
|
707
|
+
['resolve-token'],
|
|
708
|
+
['resolve-base-url'],
|
|
709
|
+
['prepare', 'https://api.textcortex.com', 'token-2'],
|
|
710
|
+
['sleep'],
|
|
711
|
+
['launch', 3, 'token-2'],
|
|
525
712
|
]);
|
|
526
713
|
});
|
|
527
714
|
|
|
528
|
-
test(
|
|
529
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
530
|
-
const zenocodeHome = path.join(tempDir,
|
|
531
|
-
const credentialsPath = path.join(zenocodeHome,
|
|
532
|
-
const scriptPath = new URL(
|
|
715
|
+
test('prepare-only refreshes stored Zenocode credentials when the selected token is invalid', async (t) => {
|
|
716
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'zenocode-refresh-'));
|
|
717
|
+
const zenocodeHome = path.join(tempDir, '.zenocode');
|
|
718
|
+
const credentialsPath = path.join(zenocodeHome, 'credentials.json');
|
|
719
|
+
const scriptPath = new URL('./run-zenocode.mjs', import.meta.url);
|
|
533
720
|
let refreshCalls = 0;
|
|
534
721
|
let refreshedAuthHeader = null;
|
|
535
722
|
let modelAuthHeader = null;
|
|
@@ -543,117 +730,142 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
|
|
|
543
730
|
credentialsPath,
|
|
544
731
|
JSON.stringify(
|
|
545
732
|
{
|
|
546
|
-
base_url:
|
|
547
|
-
access_token:
|
|
548
|
-
refresh_token:
|
|
733
|
+
base_url: 'http://127.0.0.1:1',
|
|
734
|
+
access_token: 'expired-access',
|
|
735
|
+
refresh_token: 'refresh-token',
|
|
549
736
|
expires_at: new Date(Date.now() - 60_000).toISOString(),
|
|
550
737
|
},
|
|
551
738
|
null,
|
|
552
|
-
2
|
|
553
|
-
)
|
|
739
|
+
2
|
|
740
|
+
)
|
|
554
741
|
);
|
|
555
742
|
|
|
556
743
|
const server = http.createServer((req, res) => {
|
|
557
|
-
if (req.method ===
|
|
744
|
+
if (req.method === 'POST' && req.url === '/auth/token/refresh/') {
|
|
558
745
|
refreshCalls += 1;
|
|
559
746
|
refreshedAuthHeader = req.headers.authorization || null;
|
|
560
|
-
res.writeHead(200, {
|
|
747
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
561
748
|
res.end(
|
|
562
749
|
JSON.stringify({
|
|
563
|
-
access_token:
|
|
564
|
-
refresh_token:
|
|
565
|
-
})
|
|
750
|
+
access_token: 'fresh-access',
|
|
751
|
+
refresh_token: 'fresh-refresh',
|
|
752
|
+
})
|
|
566
753
|
);
|
|
567
754
|
return;
|
|
568
755
|
}
|
|
569
756
|
|
|
570
757
|
if (
|
|
571
|
-
req.method ===
|
|
572
|
-
req.url ===
|
|
758
|
+
req.method === 'GET' &&
|
|
759
|
+
req.url === '/internal/v2/fastapi/zenocode/models/api.json'
|
|
573
760
|
) {
|
|
574
761
|
modelAuthHeader = req.headers.authorization || null;
|
|
575
|
-
if (modelAuthHeader ===
|
|
576
|
-
res.writeHead(401, {
|
|
577
|
-
res.end(JSON.stringify({ detail:
|
|
762
|
+
if (modelAuthHeader === 'Bearer stale-invalid-token') {
|
|
763
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
764
|
+
res.end(JSON.stringify({ detail: 'Invalid token format' }));
|
|
578
765
|
return;
|
|
579
766
|
}
|
|
580
767
|
|
|
581
|
-
res.writeHead(200, {
|
|
768
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
582
769
|
res.end(
|
|
583
770
|
JSON.stringify({
|
|
584
771
|
textcortex: {
|
|
585
772
|
models: {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
773
|
+
'minimax-m3-thinking': {},
|
|
774
|
+
'kimi-k2-6-non-thinking': {},
|
|
775
|
+
'kimi-k2-6-thinking': {},
|
|
776
|
+
'kimi-k2-5': {},
|
|
777
|
+
'kimi-k2-5-thinking': {},
|
|
778
|
+
'glm-5-2-non-thinking': {},
|
|
779
|
+
'glm-5-2-thinking': {},
|
|
780
|
+
'glm-5-3': {},
|
|
781
|
+
'glm-5-1-non-thinking': {},
|
|
782
|
+
'glm-5-1-thinking': {},
|
|
590
783
|
},
|
|
591
784
|
},
|
|
592
|
-
})
|
|
785
|
+
})
|
|
593
786
|
);
|
|
594
787
|
return;
|
|
595
788
|
}
|
|
596
789
|
|
|
597
|
-
res.writeHead(404, {
|
|
598
|
-
res.end(JSON.stringify({ detail:
|
|
790
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
791
|
+
res.end(JSON.stringify({ detail: 'not found' }));
|
|
599
792
|
});
|
|
600
793
|
|
|
601
|
-
await new Promise((resolve) => server.listen(0,
|
|
794
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
602
795
|
const address = server.address();
|
|
603
796
|
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
604
797
|
|
|
605
798
|
t.after(async () => {
|
|
606
|
-
await new Promise((resolve, reject) =>
|
|
799
|
+
await new Promise((resolve, reject) =>
|
|
800
|
+
server.close((error) => (error ? reject(error) : resolve()))
|
|
801
|
+
);
|
|
607
802
|
});
|
|
608
803
|
|
|
609
804
|
const result = await new Promise((resolve, reject) => {
|
|
610
805
|
const child = spawn(
|
|
611
806
|
process.execPath,
|
|
612
|
-
[scriptPath.pathname,
|
|
807
|
+
[scriptPath.pathname, '--prepare-only'],
|
|
613
808
|
{
|
|
614
809
|
cwd: tempDir,
|
|
615
810
|
env: {
|
|
616
811
|
...process.env,
|
|
617
812
|
ZENOCODE_HOME: zenocodeHome,
|
|
618
813
|
TEXTCORTEX_BASE_URL: baseUrl,
|
|
619
|
-
|
|
814
|
+
TEXTCORTEX_API_KEY: 'stale-invalid-token',
|
|
815
|
+
ZENOCODE_NO_BANNER: '1',
|
|
620
816
|
},
|
|
621
|
-
stdio: [
|
|
622
|
-
}
|
|
817
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
818
|
+
}
|
|
623
819
|
);
|
|
624
|
-
let stdout =
|
|
625
|
-
let stderr =
|
|
626
|
-
child.stdout.on(
|
|
820
|
+
let stdout = '';
|
|
821
|
+
let stderr = '';
|
|
822
|
+
child.stdout.on('data', (chunk) => {
|
|
627
823
|
stdout += String(chunk);
|
|
628
824
|
});
|
|
629
|
-
child.stderr.on(
|
|
825
|
+
child.stderr.on('data', (chunk) => {
|
|
630
826
|
stderr += String(chunk);
|
|
631
827
|
});
|
|
632
|
-
child.on(
|
|
633
|
-
child.on(
|
|
828
|
+
child.on('error', reject);
|
|
829
|
+
child.on('exit', (code, signal) =>
|
|
830
|
+
resolve({ code, signal, stdout, stderr })
|
|
831
|
+
);
|
|
634
832
|
});
|
|
635
833
|
|
|
636
|
-
assert.equal(result.code, 0);
|
|
834
|
+
assert.equal(result.code, 0, result.stderr);
|
|
637
835
|
assert.equal(refreshCalls, 1);
|
|
638
|
-
assert.equal(refreshedAuthHeader,
|
|
639
|
-
assert.equal(modelAuthHeader,
|
|
836
|
+
assert.equal(refreshedAuthHeader, 'Bearer refresh-token');
|
|
837
|
+
assert.equal(modelAuthHeader, 'Bearer fresh-access');
|
|
640
838
|
|
|
641
|
-
const savedCredentials = JSON.parse(
|
|
642
|
-
|
|
643
|
-
|
|
839
|
+
const savedCredentials = JSON.parse(
|
|
840
|
+
await fs.readFile(credentialsPath, 'utf-8')
|
|
841
|
+
);
|
|
842
|
+
assert.equal(savedCredentials.access_token, 'fresh-access');
|
|
843
|
+
assert.equal(savedCredentials.refresh_token, 'fresh-refresh');
|
|
644
844
|
|
|
645
845
|
const savedConfig = JSON.parse(
|
|
646
|
-
await fs.readFile(path.join(zenocodeHome,
|
|
846
|
+
await fs.readFile(path.join(zenocodeHome, 'zenocode.jsonc'), 'utf-8')
|
|
847
|
+
);
|
|
848
|
+
assert.equal(savedConfig.model, 'textcortex/minimax-m3-thinking');
|
|
849
|
+
|
|
850
|
+
const savedModels = JSON.parse(
|
|
851
|
+
await fs.readFile(path.join(zenocodeHome, 'models.json'), 'utf-8')
|
|
647
852
|
);
|
|
648
|
-
assert.
|
|
853
|
+
assert.ok(savedModels.textcortex.models['glm-5-2-non-thinking']);
|
|
854
|
+
assert.ok(savedModels.textcortex.models['glm-5-3']);
|
|
855
|
+
assert.equal(
|
|
856
|
+
savedModels.textcortex.models['glm-5-1-non-thinking'],
|
|
857
|
+
undefined
|
|
858
|
+
);
|
|
859
|
+
assert.equal(savedModels.textcortex.models['glm-5-1-thinking'], undefined);
|
|
649
860
|
});
|
|
650
861
|
|
|
651
|
-
test(
|
|
652
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
653
|
-
const zenocodeHome = path.join(tempDir,
|
|
654
|
-
const runtimeLogPath = path.join(tempDir,
|
|
655
|
-
const fakeRuntimePath = path.join(tempDir,
|
|
656
|
-
const scriptPath = new URL(
|
|
862
|
+
test('login launches with the newly issued token even when the environment token is stale', async (t) => {
|
|
863
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'zenocode-login-'));
|
|
864
|
+
const zenocodeHome = path.join(tempDir, '.zenocode');
|
|
865
|
+
const runtimeLogPath = path.join(tempDir, 'runtime-log.json');
|
|
866
|
+
const fakeRuntimePath = path.join(tempDir, 'fake-opencode');
|
|
867
|
+
const scriptPath = new URL('./run-zenocode.mjs', import.meta.url);
|
|
868
|
+
let modelAuthHeader = null;
|
|
657
869
|
|
|
658
870
|
t.after(async () => {
|
|
659
871
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
@@ -671,6 +883,7 @@ async function main() {
|
|
|
671
883
|
env: {
|
|
672
884
|
OPENCODE_CONFIG: process.env.OPENCODE_CONFIG,
|
|
673
885
|
OPENCODE_TUI_CONFIG: process.env.OPENCODE_TUI_CONFIG,
|
|
886
|
+
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE,
|
|
674
887
|
TEXTCORTEX_API_KEY: process.env.TEXTCORTEX_API_KEY,
|
|
675
888
|
},
|
|
676
889
|
};
|
|
@@ -682,78 +895,91 @@ main().catch((error) => {
|
|
|
682
895
|
process.exit(1);
|
|
683
896
|
});
|
|
684
897
|
`,
|
|
685
|
-
|
|
898
|
+
'utf-8'
|
|
686
899
|
);
|
|
687
900
|
await fs.chmod(fakeRuntimePath, 0o755);
|
|
688
901
|
|
|
689
902
|
const server = http.createServer((req, res) => {
|
|
690
903
|
if (
|
|
691
|
-
req.method ===
|
|
692
|
-
req.url ===
|
|
904
|
+
req.method === 'POST' &&
|
|
905
|
+
req.url === '/internal/v2/fastapi/zenocode/oauth2/initiate'
|
|
693
906
|
) {
|
|
694
|
-
res.writeHead(200, {
|
|
907
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
695
908
|
res.end(
|
|
696
909
|
JSON.stringify({
|
|
697
910
|
data: {
|
|
698
|
-
device_code:
|
|
699
|
-
user_code:
|
|
700
|
-
verification_url_complete:
|
|
911
|
+
device_code: 'device-code',
|
|
912
|
+
user_code: 'ABCD-1234',
|
|
913
|
+
verification_url_complete: 'https://textcortex.example/verify',
|
|
701
914
|
interval: 0,
|
|
702
915
|
expires_in: 30,
|
|
703
916
|
},
|
|
704
|
-
})
|
|
917
|
+
})
|
|
705
918
|
);
|
|
706
919
|
return;
|
|
707
920
|
}
|
|
708
921
|
|
|
709
922
|
if (
|
|
710
|
-
req.method ===
|
|
711
|
-
req.url ===
|
|
923
|
+
req.method === 'POST' &&
|
|
924
|
+
req.url === '/internal/v2/fastapi/zenocode/oauth2/token'
|
|
712
925
|
) {
|
|
713
|
-
res.writeHead(200, {
|
|
926
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
714
927
|
res.end(
|
|
715
928
|
JSON.stringify({
|
|
716
929
|
data: {
|
|
717
|
-
access_token:
|
|
718
|
-
refresh_token:
|
|
719
|
-
auth_id:
|
|
930
|
+
access_token: 'fresh-access',
|
|
931
|
+
refresh_token: 'fresh-refresh',
|
|
932
|
+
auth_id: 'auth_123',
|
|
720
933
|
},
|
|
721
|
-
})
|
|
934
|
+
})
|
|
722
935
|
);
|
|
723
936
|
return;
|
|
724
937
|
}
|
|
725
938
|
|
|
726
939
|
if (
|
|
727
|
-
req.method ===
|
|
728
|
-
req.url ===
|
|
940
|
+
req.method === 'GET' &&
|
|
941
|
+
req.url === '/internal/v2/fastapi/zenocode/models/api.json'
|
|
729
942
|
) {
|
|
730
|
-
|
|
943
|
+
modelAuthHeader = req.headers.authorization || null;
|
|
944
|
+
if (modelAuthHeader !== 'Bearer fresh-access') {
|
|
945
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
946
|
+
res.end(JSON.stringify({ detail: 'Invalid token format' }));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
731
951
|
res.end(
|
|
732
952
|
JSON.stringify({
|
|
733
953
|
textcortex: {
|
|
734
954
|
models: {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
955
|
+
'minimax-m3-thinking': {},
|
|
956
|
+
'kimi-k2-6-non-thinking': {},
|
|
957
|
+
'kimi-k2-6-thinking': {},
|
|
958
|
+
'kimi-k2-5': {},
|
|
959
|
+
'kimi-k2-5-thinking': {},
|
|
960
|
+
'glm-5-2-non-thinking': {},
|
|
961
|
+
'glm-5-2-thinking': {},
|
|
962
|
+
'glm-5-3': {},
|
|
963
|
+
'glm-5-1-non-thinking': {},
|
|
964
|
+
'glm-5-1-thinking': {},
|
|
739
965
|
},
|
|
740
966
|
},
|
|
741
|
-
})
|
|
967
|
+
})
|
|
742
968
|
);
|
|
743
969
|
return;
|
|
744
970
|
}
|
|
745
971
|
|
|
746
|
-
res.writeHead(404, {
|
|
747
|
-
res.end(JSON.stringify({ detail:
|
|
972
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
973
|
+
res.end(JSON.stringify({ detail: 'not found' }));
|
|
748
974
|
});
|
|
749
975
|
|
|
750
|
-
await new Promise((resolve) => server.listen(0,
|
|
976
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
751
977
|
const address = server.address();
|
|
752
978
|
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
753
979
|
|
|
754
980
|
t.after(async () => {
|
|
755
981
|
await new Promise((resolve, reject) =>
|
|
756
|
-
server.close((error) => (error ? reject(error) : resolve()))
|
|
982
|
+
server.close((error) => (error ? reject(error) : resolve()))
|
|
757
983
|
);
|
|
758
984
|
});
|
|
759
985
|
|
|
@@ -762,117 +988,147 @@ main().catch((error) => {
|
|
|
762
988
|
process.execPath,
|
|
763
989
|
[
|
|
764
990
|
scriptPath.pathname,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
991
|
+
'login',
|
|
992
|
+
'--email',
|
|
993
|
+
'person@example.com',
|
|
994
|
+
'--no-launch-browser',
|
|
769
995
|
],
|
|
770
996
|
{
|
|
771
997
|
cwd: tempDir,
|
|
772
998
|
env: {
|
|
773
999
|
...process.env,
|
|
774
1000
|
ZENOCODE_HOME: zenocodeHome,
|
|
775
|
-
ZENOCODE_NO_BANNER:
|
|
1001
|
+
ZENOCODE_NO_BANNER: '1',
|
|
776
1002
|
ZENOCODE_OPENCODE_BIN_PATH: fakeRuntimePath,
|
|
777
1003
|
TEXTCORTEX_BASE_URL: baseUrl,
|
|
1004
|
+
TEXTCORTEX_API_KEY: 'stale-invalid-token',
|
|
778
1005
|
RUNTIME_LOG_PATH: runtimeLogPath,
|
|
779
1006
|
},
|
|
780
|
-
stdio: [
|
|
781
|
-
}
|
|
1007
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1008
|
+
}
|
|
782
1009
|
);
|
|
783
|
-
let stdout =
|
|
784
|
-
let stderr =
|
|
785
|
-
child.stdout.on(
|
|
1010
|
+
let stdout = '';
|
|
1011
|
+
let stderr = '';
|
|
1012
|
+
child.stdout.on('data', (chunk) => {
|
|
786
1013
|
stdout += String(chunk);
|
|
787
1014
|
});
|
|
788
|
-
child.stderr.on(
|
|
1015
|
+
child.stderr.on('data', (chunk) => {
|
|
789
1016
|
stderr += String(chunk);
|
|
790
1017
|
});
|
|
791
|
-
child.on(
|
|
792
|
-
child.on(
|
|
1018
|
+
child.on('error', reject);
|
|
1019
|
+
child.on('exit', (code, signal) =>
|
|
1020
|
+
resolve({ code, signal, stdout, stderr })
|
|
1021
|
+
);
|
|
793
1022
|
});
|
|
794
1023
|
|
|
795
1024
|
assert.equal(result.code, 0);
|
|
796
1025
|
assert.match(result.stdout, /Login successful for auth_123/);
|
|
797
1026
|
assert.match(result.stdout, /Zenocode config ready at/);
|
|
1027
|
+
assert.equal(modelAuthHeader, 'Bearer fresh-access');
|
|
798
1028
|
|
|
799
|
-
const runtimeInvocation = JSON.parse(
|
|
1029
|
+
const runtimeInvocation = JSON.parse(
|
|
1030
|
+
await fs.readFile(runtimeLogPath, 'utf-8')
|
|
1031
|
+
);
|
|
800
1032
|
assert.deepEqual(runtimeInvocation.args, []);
|
|
801
|
-
assert.equal(runtimeInvocation.env.TEXTCORTEX_API_KEY,
|
|
1033
|
+
assert.equal(runtimeInvocation.env.TEXTCORTEX_API_KEY, 'fresh-access');
|
|
802
1034
|
|
|
803
1035
|
const opencodeConfig = JSON.parse(
|
|
804
|
-
await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG,
|
|
1036
|
+
await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG, 'utf-8')
|
|
1037
|
+
);
|
|
1038
|
+
assert.equal(opencodeConfig.theme, 'system');
|
|
1039
|
+
assert.equal(opencodeConfig.autoupdate, false);
|
|
1040
|
+
assert.equal(opencodeConfig.model, 'textcortex/minimax-m3-thinking');
|
|
1041
|
+
assert.equal(runtimeInvocation.env.OPENCODE_DISABLE_AUTOUPDATE, 'true');
|
|
1042
|
+
|
|
1043
|
+
const modelsConfig = JSON.parse(
|
|
1044
|
+
await fs.readFile(path.join(zenocodeHome, 'models.json'), 'utf-8')
|
|
805
1045
|
);
|
|
806
|
-
assert.
|
|
807
|
-
assert.
|
|
1046
|
+
assert.ok(modelsConfig.textcortex.models['glm-5-2-non-thinking']);
|
|
1047
|
+
assert.ok(modelsConfig.textcortex.models['glm-5-3']);
|
|
1048
|
+
assert.equal(
|
|
1049
|
+
modelsConfig.textcortex.models['glm-5-1-non-thinking'],
|
|
1050
|
+
undefined
|
|
1051
|
+
);
|
|
1052
|
+
assert.equal(modelsConfig.textcortex.models['glm-5-1-thinking'], undefined);
|
|
808
1053
|
|
|
809
1054
|
const tuiConfig = JSON.parse(
|
|
810
|
-
await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG,
|
|
1055
|
+
await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, 'utf-8')
|
|
811
1056
|
);
|
|
812
|
-
assert.equal(tuiConfig.theme,
|
|
1057
|
+
assert.equal(tuiConfig.theme, 'system');
|
|
813
1058
|
});
|
|
814
1059
|
|
|
815
|
-
test(
|
|
816
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
817
|
-
const zenocodeHome = path.join(tempDir,
|
|
818
|
-
const codecortexHome = path.join(tempDir,
|
|
819
|
-
const homeCredentialsPath = path.join(tempDir,
|
|
820
|
-
const runtimeCredentialsPath = path.join(zenocodeHome,
|
|
821
|
-
const legacyRuntimeCredentialsPath = path.join(
|
|
822
|
-
|
|
1060
|
+
test('logout removes runtime credentials and blocks shared fallback credentials', async (t) => {
|
|
1061
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'zenocode-logout-'));
|
|
1062
|
+
const zenocodeHome = path.join(tempDir, '.zenocode');
|
|
1063
|
+
const codecortexHome = path.join(tempDir, '.codecortex');
|
|
1064
|
+
const homeCredentialsPath = path.join(tempDir, '.credentials.json');
|
|
1065
|
+
const runtimeCredentialsPath = path.join(zenocodeHome, 'credentials.json');
|
|
1066
|
+
const legacyRuntimeCredentialsPath = path.join(
|
|
1067
|
+
codecortexHome,
|
|
1068
|
+
'credentials.json'
|
|
1069
|
+
);
|
|
1070
|
+
const scriptPath = new URL('./run-zenocode.mjs', import.meta.url);
|
|
823
1071
|
|
|
824
1072
|
t.after(async () => {
|
|
825
1073
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
826
1074
|
});
|
|
827
1075
|
|
|
828
1076
|
await fs.mkdir(path.dirname(runtimeCredentialsPath), { recursive: true });
|
|
829
|
-
await fs.mkdir(path.dirname(legacyRuntimeCredentialsPath), {
|
|
830
|
-
|
|
1077
|
+
await fs.mkdir(path.dirname(legacyRuntimeCredentialsPath), {
|
|
1078
|
+
recursive: true,
|
|
1079
|
+
});
|
|
1080
|
+
await fs.writeFile(
|
|
1081
|
+
runtimeCredentialsPath,
|
|
1082
|
+
JSON.stringify({ access_token: 'current' })
|
|
1083
|
+
);
|
|
831
1084
|
await fs.writeFile(
|
|
832
1085
|
legacyRuntimeCredentialsPath,
|
|
833
|
-
JSON.stringify({ access_token:
|
|
1086
|
+
JSON.stringify({ access_token: 'legacy' })
|
|
1087
|
+
);
|
|
1088
|
+
await fs.writeFile(
|
|
1089
|
+
homeCredentialsPath,
|
|
1090
|
+
JSON.stringify({ access_token: 'home' })
|
|
834
1091
|
);
|
|
835
|
-
await fs.writeFile(homeCredentialsPath, JSON.stringify({ access_token: "home" }));
|
|
836
1092
|
|
|
837
1093
|
const result = await new Promise((resolve, reject) => {
|
|
838
|
-
const child = spawn(
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
HOME: tempDir,
|
|
846
|
-
ZENOCODE_HOME: zenocodeHome,
|
|
847
|
-
CODECORTEX_HOME: codecortexHome,
|
|
848
|
-
},
|
|
849
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1094
|
+
const child = spawn(process.execPath, [scriptPath.pathname, 'logout'], {
|
|
1095
|
+
cwd: tempDir,
|
|
1096
|
+
env: {
|
|
1097
|
+
...process.env,
|
|
1098
|
+
HOME: tempDir,
|
|
1099
|
+
ZENOCODE_HOME: zenocodeHome,
|
|
1100
|
+
CODECORTEX_HOME: codecortexHome,
|
|
850
1101
|
},
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
let
|
|
854
|
-
|
|
1102
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1103
|
+
});
|
|
1104
|
+
let stdout = '';
|
|
1105
|
+
let stderr = '';
|
|
1106
|
+
child.stdout.on('data', (chunk) => {
|
|
855
1107
|
stdout += String(chunk);
|
|
856
1108
|
});
|
|
857
|
-
child.stderr.on(
|
|
1109
|
+
child.stderr.on('data', (chunk) => {
|
|
858
1110
|
stderr += String(chunk);
|
|
859
1111
|
});
|
|
860
|
-
child.on(
|
|
861
|
-
child.on(
|
|
1112
|
+
child.on('error', reject);
|
|
1113
|
+
child.on('exit', (code, signal) =>
|
|
1114
|
+
resolve({ code, signal, stdout, stderr })
|
|
1115
|
+
);
|
|
862
1116
|
});
|
|
863
1117
|
|
|
864
|
-
assert.equal(result.code, 0);
|
|
865
|
-
await assert.rejects(fs.stat(runtimeCredentialsPath), { code:
|
|
866
|
-
await assert.rejects(fs.stat(legacyRuntimeCredentialsPath), {
|
|
1118
|
+
assert.equal(result.code, 0, result.stderr);
|
|
1119
|
+
await assert.rejects(fs.stat(runtimeCredentialsPath), { code: 'ENOENT' });
|
|
1120
|
+
await assert.rejects(fs.stat(legacyRuntimeCredentialsPath), {
|
|
1121
|
+
code: 'ENOENT',
|
|
1122
|
+
});
|
|
867
1123
|
assert.deepEqual(
|
|
868
|
-
JSON.parse(await fs.readFile(homeCredentialsPath,
|
|
869
|
-
{ access_token:
|
|
1124
|
+
JSON.parse(await fs.readFile(homeCredentialsPath, 'utf-8')),
|
|
1125
|
+
{ access_token: 'home' }
|
|
870
1126
|
);
|
|
871
1127
|
|
|
872
1128
|
const prepareOnlyResult = await new Promise((resolve, reject) => {
|
|
873
1129
|
const child = spawn(
|
|
874
1130
|
process.execPath,
|
|
875
|
-
[scriptPath.pathname,
|
|
1131
|
+
[scriptPath.pathname, '--prepare-only'],
|
|
876
1132
|
{
|
|
877
1133
|
cwd: tempDir,
|
|
878
1134
|
env: {
|
|
@@ -880,23 +1136,25 @@ test("logout removes runtime credentials and blocks shared fallback credentials"
|
|
|
880
1136
|
HOME: tempDir,
|
|
881
1137
|
ZENOCODE_HOME: zenocodeHome,
|
|
882
1138
|
CODECORTEX_HOME: codecortexHome,
|
|
883
|
-
ZENOCODE_NO_BANNER:
|
|
1139
|
+
ZENOCODE_NO_BANNER: '1',
|
|
884
1140
|
},
|
|
885
|
-
stdio: [
|
|
886
|
-
}
|
|
1141
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1142
|
+
}
|
|
887
1143
|
);
|
|
888
|
-
let stdout =
|
|
889
|
-
let stderr =
|
|
890
|
-
child.stdout.on(
|
|
1144
|
+
let stdout = '';
|
|
1145
|
+
let stderr = '';
|
|
1146
|
+
child.stdout.on('data', (chunk) => {
|
|
891
1147
|
stdout += String(chunk);
|
|
892
1148
|
});
|
|
893
|
-
child.stderr.on(
|
|
1149
|
+
child.stderr.on('data', (chunk) => {
|
|
894
1150
|
stderr += String(chunk);
|
|
895
1151
|
});
|
|
896
|
-
child.on(
|
|
897
|
-
child.on(
|
|
1152
|
+
child.on('error', reject);
|
|
1153
|
+
child.on('exit', (code, signal) =>
|
|
1154
|
+
resolve({ code, signal, stdout, stderr })
|
|
1155
|
+
);
|
|
898
1156
|
});
|
|
899
1157
|
|
|
900
|
-
assert.equal(prepareOnlyResult.code, 1);
|
|
1158
|
+
assert.equal(prepareOnlyResult.code, 1, prepareOnlyResult.stderr);
|
|
901
1159
|
assert.match(prepareOnlyResult.stderr, /Missing API token/);
|
|
902
1160
|
});
|