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