w-dispatch-ai 1.0.0 → 1.0.2
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 +108 -6
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +8 -4
- package/docs/adapters.mjs.html +5 -3
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +406 -0
- package/docs/dispatchAntigravity.mjs.html +264 -0
- package/docs/dispatchClaude.mjs.html +2 -2
- package/docs/dispatchCodex.mjs.html +2 -2
- package/docs/dispatchOpencode.mjs.html +2 -2
- package/docs/getCliArgs.mjs.html +2 -2
- package/docs/getErrorResult.mjs.html +2 -2
- package/docs/global.html +1600 -6
- package/docs/index.html +2 -2
- package/g.mjs +46 -2
- package/package.json +1 -1
- package/src/WDispatchAi.mjs +6 -2
- package/src/adapters.mjs +3 -1
- package/src/dispatchAiFallback.mjs +334 -0
- package/src/dispatchAntigravity.mjs +192 -0
- package/test/tools/fakeCliForTest.mjs +8 -0
- package/test/unit-WDispatchAi.test.mjs +13 -6
- package/test/unit-adapters.test.mjs +5 -3
- package/test/unit-dispatchAi.test.mjs +7 -3
- package/test/unit-dispatchAiFallback.test.mjs +422 -0
- package/test/unit-dispatchAntigravity.test.mjs +173 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import dispatchAiFallback from '../src/dispatchAiFallback.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
//assertKey, 由假CLI回聲之OPENCODE_AUTH_CONTENT解出本次注入之金鑰
|
|
7
|
+
let getInjectedKey = (stdout) => {
|
|
8
|
+
let o = JSON.parse(stdout)
|
|
9
|
+
let auth = o.env.OPENCODE_AUTH_CONTENT
|
|
10
|
+
if (auth === '') {
|
|
11
|
+
return ''
|
|
12
|
+
}
|
|
13
|
+
let j = JSON.parse(auth)
|
|
14
|
+
return Object.values(j)[0].key
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('dispatchAiFallback', function() {
|
|
19
|
+
|
|
20
|
+
let fake = null
|
|
21
|
+
|
|
22
|
+
before(function() {
|
|
23
|
+
fake = createFakeCli('fake-fallback')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
after(function() {
|
|
27
|
+
if (fake) {
|
|
28
|
+
fake.clean()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('prompt非有效字串時回傳錯誤結果物件且tried為空', async function() {
|
|
33
|
+
let t = await dispatchAiFallback('', { providers: [{ kind: 'claude' }] })
|
|
34
|
+
let r = [t.ok, t.error, t.tried]
|
|
35
|
+
let rr = [false, 'prompt must be a non-empty string', []]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('providers非有效陣列時回傳錯誤結果物件', async function() {
|
|
40
|
+
let r = []
|
|
41
|
+
for (let providers of [null, undefined, [], {}, 'abc', [null, 'x', 123]]) {
|
|
42
|
+
let t = await dispatchAiFallback('abc', { providers })
|
|
43
|
+
r.push([t.ok, t.error])
|
|
44
|
+
}
|
|
45
|
+
let rr = [
|
|
46
|
+
[false, 'providers must be a non-empty array'],
|
|
47
|
+
[false, 'providers must be a non-empty array'],
|
|
48
|
+
[false, 'providers must be a non-empty array'],
|
|
49
|
+
[false, 'providers must be a non-empty array'],
|
|
50
|
+
[false, 'providers must be a non-empty array'],
|
|
51
|
+
[false, 'providers must be a non-empty array'],
|
|
52
|
+
]
|
|
53
|
+
assert.strict.deepEqual(r, rr)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('某把金鑰失敗自動換組內下一把並成功', async function() {
|
|
57
|
+
let t = await dispatchAiFallback('abc', {
|
|
58
|
+
providers: [
|
|
59
|
+
{
|
|
60
|
+
id: 'g-rotate',
|
|
61
|
+
kind: 'opencode',
|
|
62
|
+
exe: fake.exe,
|
|
63
|
+
provider: 'opencode',
|
|
64
|
+
keys: ['sk-bad-aaa', 'sk-good-bbb'],
|
|
65
|
+
extraArgs: ['--fake-fail-key=sk-bad'],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
let r = [
|
|
70
|
+
t.ok,
|
|
71
|
+
t.providerId,
|
|
72
|
+
t.keyIndex,
|
|
73
|
+
getInjectedKey(t.stdout),
|
|
74
|
+
t.tried.map((x) => [x.keyId, x.outcome]),
|
|
75
|
+
]
|
|
76
|
+
let rr = [
|
|
77
|
+
true,
|
|
78
|
+
'g-rotate',
|
|
79
|
+
1,
|
|
80
|
+
'sk-good-bbb',
|
|
81
|
+
[['g-rotate#0', 'next-key'], ['g-rotate#1', 'ok']],
|
|
82
|
+
]
|
|
83
|
+
assert.strict.deepEqual(r, rr)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('成功亦推進游標, 多把金鑰跨次呼叫輪替均攤', async function() {
|
|
87
|
+
let providers = [{
|
|
88
|
+
id: 'g-cursor',
|
|
89
|
+
kind: 'opencode',
|
|
90
|
+
exe: fake.exe,
|
|
91
|
+
provider: 'opencode',
|
|
92
|
+
keys: ['sk-k0', 'sk-k1', 'sk-k2'],
|
|
93
|
+
}]
|
|
94
|
+
let r = []
|
|
95
|
+
for (let i = 0; i < 4; i++) {
|
|
96
|
+
let t = await dispatchAiFallback('abc', { providers })
|
|
97
|
+
r.push(getInjectedKey(t.stdout))
|
|
98
|
+
}
|
|
99
|
+
let rr = ['sk-k0', 'sk-k1', 'sk-k2', 'sk-k0']
|
|
100
|
+
assert.strict.deepEqual(r, rr)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('store注入時游標經store持久化, 各群組游標互不干擾', async function() {
|
|
104
|
+
let stored = { cursors: {} }
|
|
105
|
+
let store = {
|
|
106
|
+
get: () => stored,
|
|
107
|
+
set: (s) => {
|
|
108
|
+
stored = s
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
let providers = [{
|
|
112
|
+
id: 'g-store-a',
|
|
113
|
+
kind: 'opencode',
|
|
114
|
+
exe: fake.exe,
|
|
115
|
+
provider: 'opencode',
|
|
116
|
+
keys: ['sk-a0', 'sk-a1'],
|
|
117
|
+
}]
|
|
118
|
+
let t1 = await dispatchAiFallback('abc', { providers, store })
|
|
119
|
+
let t2 = await dispatchAiFallback('abc', { providers, store })
|
|
120
|
+
let r = [
|
|
121
|
+
getInjectedKey(t1.stdout),
|
|
122
|
+
getInjectedKey(t2.stdout),
|
|
123
|
+
stored.cursors['g-store-a'],
|
|
124
|
+
stored.cursors['g-store-b'],
|
|
125
|
+
]
|
|
126
|
+
let rr = ['sk-a0', 'sk-a1', 0, undefined]
|
|
127
|
+
assert.strict.deepEqual(r, rr)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('整組金鑰用盡時遞補下一組', async function() {
|
|
131
|
+
let t = await dispatchAiFallback('abc', {
|
|
132
|
+
providers: [
|
|
133
|
+
{
|
|
134
|
+
id: 'g-dead',
|
|
135
|
+
kind: 'opencode',
|
|
136
|
+
exe: fake.exe,
|
|
137
|
+
provider: 'opencode',
|
|
138
|
+
keys: ['sk-bad-1', 'sk-bad-2'],
|
|
139
|
+
extraArgs: ['--fake-fail-key=sk-bad'],
|
|
140
|
+
},
|
|
141
|
+
{ id: 'g-alive', kind: 'claude', exe: fake.exe },
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
let o = JSON.parse(t.stdout)
|
|
145
|
+
let r = [
|
|
146
|
+
t.ok,
|
|
147
|
+
t.providerId,
|
|
148
|
+
t.keyIndex,
|
|
149
|
+
o.args[0], //claude轉接器之固定首旗標
|
|
150
|
+
t.tried.map((x) => [x.providerId, x.outcome]),
|
|
151
|
+
]
|
|
152
|
+
let rr = [
|
|
153
|
+
true,
|
|
154
|
+
'g-alive',
|
|
155
|
+
null,
|
|
156
|
+
'-p',
|
|
157
|
+
[['g-dead', 'next-key'], ['g-dead', 'next-key'], ['g-alive', 'ok']],
|
|
158
|
+
]
|
|
159
|
+
assert.strict.deepEqual(r, rr)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('逾時為與金鑰無關之失敗, 一把即整組跳過不逐把空耗', async function() {
|
|
163
|
+
let t = await dispatchAiFallback('abc', {
|
|
164
|
+
providers: [
|
|
165
|
+
{
|
|
166
|
+
id: 'g-hang',
|
|
167
|
+
kind: 'opencode',
|
|
168
|
+
exe: fake.exe,
|
|
169
|
+
provider: 'opencode',
|
|
170
|
+
keys: ['sk-h0', 'sk-h1', 'sk-h2'],
|
|
171
|
+
extraArgs: ['--fake-sleep=10000'],
|
|
172
|
+
timeoutMs: 1000,
|
|
173
|
+
},
|
|
174
|
+
{ id: 'g-next', kind: 'claude', exe: fake.exe },
|
|
175
|
+
],
|
|
176
|
+
})
|
|
177
|
+
let r = [
|
|
178
|
+
t.ok,
|
|
179
|
+
t.providerId,
|
|
180
|
+
t.tried.length, //hang組僅1筆而非3筆
|
|
181
|
+
t.tried[0].outcome,
|
|
182
|
+
t.tried[0].error.indexOf('TIMEOUT') === 0,
|
|
183
|
+
]
|
|
184
|
+
let rr = [true, 'g-next', 2, 'skip-group', true]
|
|
185
|
+
assert.strict.deepEqual(r, rr)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('參數錯誤(code=2)整組跳過', async function() {
|
|
189
|
+
let t = await dispatchAiFallback('abc', {
|
|
190
|
+
providers: [
|
|
191
|
+
{
|
|
192
|
+
id: 'g-arg',
|
|
193
|
+
kind: 'opencode',
|
|
194
|
+
exe: fake.exe,
|
|
195
|
+
provider: 'opencode',
|
|
196
|
+
keys: ['sk-x0', 'sk-x1'],
|
|
197
|
+
extraArgs: ['--fake-exit=2'],
|
|
198
|
+
},
|
|
199
|
+
{ id: 'g-ok', kind: 'codex', exe: fake.exe },
|
|
200
|
+
],
|
|
201
|
+
})
|
|
202
|
+
let r = [t.ok, t.providerId, t.tried.map((x) => x.outcome)]
|
|
203
|
+
let rr = [true, 'g-ok', ['skip-group', 'ok']]
|
|
204
|
+
assert.strict.deepEqual(r, rr)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('輸出未過驗證整組跳過, 條目validate覆寫共用預設', async function() {
|
|
208
|
+
let t = await dispatchAiFallback('abc', {
|
|
209
|
+
providers: [
|
|
210
|
+
{
|
|
211
|
+
id: 'g-badout',
|
|
212
|
+
kind: 'opencode',
|
|
213
|
+
exe: fake.exe,
|
|
214
|
+
provider: 'opencode',
|
|
215
|
+
keys: ['sk-v0', 'sk-v1'],
|
|
216
|
+
validate: 'min:100000',
|
|
217
|
+
},
|
|
218
|
+
{ id: 'g-anyout', kind: 'claude', exe: fake.exe },
|
|
219
|
+
],
|
|
220
|
+
})
|
|
221
|
+
let r = [
|
|
222
|
+
t.ok,
|
|
223
|
+
t.providerId,
|
|
224
|
+
t.tried.map((x) => [x.outcome, x.error || null]),
|
|
225
|
+
]
|
|
226
|
+
let rr = [
|
|
227
|
+
true,
|
|
228
|
+
'g-anyout',
|
|
229
|
+
[['skip-group', 'OUTPUT_VALIDATION_FAILED'], ['ok', null]],
|
|
230
|
+
]
|
|
231
|
+
assert.strict.deepEqual(r, rr)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('執行檔不存在(ENOENT)整組跳過', async function() {
|
|
235
|
+
let t = await dispatchAiFallback('abc', {
|
|
236
|
+
providers: [
|
|
237
|
+
{ id: 'g-noexe', kind: 'claude', exe: 'no-such-cli-for-test', keys: ['sk-n0', 'sk-n1'] },
|
|
238
|
+
{ id: 'g-hasexe', kind: 'claude', exe: fake.exe },
|
|
239
|
+
],
|
|
240
|
+
})
|
|
241
|
+
let r = [t.ok, t.providerId, t.tried.map((x) => x.outcome)]
|
|
242
|
+
let rr = [true, 'g-hasexe', ['skip-group', 'ok']]
|
|
243
|
+
assert.strict.deepEqual(r, rr)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('kind無效之條目整組跳過, 不中斷後續遞補', async function() {
|
|
247
|
+
let t = await dispatchAiFallback('abc', {
|
|
248
|
+
providers: [
|
|
249
|
+
{ id: 'g-unknown', kind: 'gemini' },
|
|
250
|
+
{ id: 'g-known', kind: 'claude', exe: fake.exe },
|
|
251
|
+
],
|
|
252
|
+
})
|
|
253
|
+
let r = [
|
|
254
|
+
t.ok,
|
|
255
|
+
t.providerId,
|
|
256
|
+
t.tried[0].outcome,
|
|
257
|
+
t.tried[0].error.indexOf('unknown ai kind') === 0,
|
|
258
|
+
]
|
|
259
|
+
let rr = [true, 'g-known', 'skip-group', true]
|
|
260
|
+
assert.strict.deepEqual(r, rr)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('全數供應商失敗時回傳最後一筆失敗結果與完整歷程', async function() {
|
|
264
|
+
let t = await dispatchAiFallback('abc', {
|
|
265
|
+
providers: [
|
|
266
|
+
{ id: 'g-f1', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
267
|
+
{ id: 'g-f2', kind: 'codex', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
268
|
+
],
|
|
269
|
+
})
|
|
270
|
+
let r = [
|
|
271
|
+
t.ok,
|
|
272
|
+
t.code,
|
|
273
|
+
t.error,
|
|
274
|
+
t.providerId,
|
|
275
|
+
t.tried.map((x) => [x.providerId, x.outcome]),
|
|
276
|
+
]
|
|
277
|
+
let rr = [
|
|
278
|
+
false,
|
|
279
|
+
1,
|
|
280
|
+
'Exit code 1',
|
|
281
|
+
'g-f2',
|
|
282
|
+
[['g-f1', 'next-key'], ['g-f2', 'next-key']],
|
|
283
|
+
]
|
|
284
|
+
assert.strict.deepEqual(r, rr)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('預算不足一次最低嘗試時停止並回報budget exhausted', async function() {
|
|
288
|
+
let t = await dispatchAiFallback('abc', {
|
|
289
|
+
providers: [{ id: 'g-nobudget', kind: 'claude', exe: fake.exe }],
|
|
290
|
+
budgetMs: 50,
|
|
291
|
+
minAttemptMs: 100,
|
|
292
|
+
})
|
|
293
|
+
let r = [t.ok, t.error, t.tried.map((x) => x.outcome)]
|
|
294
|
+
let rr = [false, 'budget exhausted', ['budget-out']]
|
|
295
|
+
assert.strict.deepEqual(r, rr)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('剩餘預算會壓進attempt之timeoutMs', async function() {
|
|
299
|
+
let t = await dispatchAiFallback('abc', {
|
|
300
|
+
providers: [{
|
|
301
|
+
id: 'g-clamp',
|
|
302
|
+
kind: 'claude',
|
|
303
|
+
exe: fake.exe,
|
|
304
|
+
extraArgs: ['--fake-sleep=10000'], //timeoutMs未給, 預設120000, 應被預算4000封頂
|
|
305
|
+
}],
|
|
306
|
+
budgetMs: 4000,
|
|
307
|
+
minAttemptMs: 100,
|
|
308
|
+
})
|
|
309
|
+
let r = [
|
|
310
|
+
t.ok,
|
|
311
|
+
t.tried[0].outcome,
|
|
312
|
+
t.error.indexOf('TIMEOUT') === 0,
|
|
313
|
+
t.durationMs < 9000, //遠小於fake-sleep之10000, 證明timeout被封頂
|
|
314
|
+
]
|
|
315
|
+
let rr = [false, 'skip-group', true, true]
|
|
316
|
+
assert.strict.deepEqual(r, rr)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('onEvent依序回報事件, 回調拋出例外不影響主流程', async function() {
|
|
320
|
+
let evs = []
|
|
321
|
+
let t = await dispatchAiFallback('abc', {
|
|
322
|
+
providers: [{
|
|
323
|
+
id: 'g-ev',
|
|
324
|
+
kind: 'opencode',
|
|
325
|
+
exe: fake.exe,
|
|
326
|
+
provider: 'opencode',
|
|
327
|
+
keys: ['sk-bad-e0', 'sk-good-e1'],
|
|
328
|
+
extraArgs: ['--fake-fail-key=sk-bad'],
|
|
329
|
+
}],
|
|
330
|
+
onEvent: (ev) => {
|
|
331
|
+
evs.push([ev.type, ev.providerId, ev.keyIndex])
|
|
332
|
+
throw new Error('callback error should not break the loop')
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
let r = [t.ok, evs]
|
|
336
|
+
let rr = [
|
|
337
|
+
true,
|
|
338
|
+
[
|
|
339
|
+
['try', 'g-ev', 0],
|
|
340
|
+
['next-key', 'g-ev', 0],
|
|
341
|
+
['try', 'g-ev', 1],
|
|
342
|
+
['ok', 'g-ev', 1],
|
|
343
|
+
],
|
|
344
|
+
]
|
|
345
|
+
assert.strict.deepEqual(r, rr)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('條目即opt, config與provider等鍵原樣透傳對應轉接器', async function() {
|
|
349
|
+
let config = { provider: { 'agnes-ai': { npm: '@ai-sdk/openai-compatible' } } }
|
|
350
|
+
let t = await dispatchAiFallback('abc', {
|
|
351
|
+
providers: [{
|
|
352
|
+
id: 'g-agnes',
|
|
353
|
+
kind: 'opencode',
|
|
354
|
+
exe: fake.exe,
|
|
355
|
+
model: 'agnes-ai/agnes-2.0-flash',
|
|
356
|
+
provider: 'agnes-ai',
|
|
357
|
+
keys: ['sk-agnes-0'],
|
|
358
|
+
config,
|
|
359
|
+
}],
|
|
360
|
+
})
|
|
361
|
+
let o = JSON.parse(t.stdout)
|
|
362
|
+
let r = [
|
|
363
|
+
t.ok,
|
|
364
|
+
JSON.parse(o.env.OPENCODE_CONFIG_CONTENT),
|
|
365
|
+
JSON.parse(o.env.OPENCODE_AUTH_CONTENT),
|
|
366
|
+
o.args.includes('agnes-ai/agnes-2.0-flash'),
|
|
367
|
+
]
|
|
368
|
+
let rr = [
|
|
369
|
+
true,
|
|
370
|
+
config,
|
|
371
|
+
{ 'agnes-ai': { type: 'api', key: 'sk-agnes-0' } },
|
|
372
|
+
true,
|
|
373
|
+
]
|
|
374
|
+
assert.strict.deepEqual(r, rr)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('四層情境: 組內輪替與跨組遞補複合', async function() {
|
|
378
|
+
//deepseek兩把全敗(額度型) → agnes第1把敗換第2把成功
|
|
379
|
+
let t = await dispatchAiFallback('abc', {
|
|
380
|
+
providers: [
|
|
381
|
+
{
|
|
382
|
+
id: 'g-ds',
|
|
383
|
+
kind: 'opencode',
|
|
384
|
+
exe: fake.exe,
|
|
385
|
+
provider: 'opencode',
|
|
386
|
+
keys: ['sk-dsbad-0', 'sk-dsbad-1'],
|
|
387
|
+
extraArgs: ['--fake-fail-key=sk-dsbad'],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 'g-ag',
|
|
391
|
+
kind: 'opencode',
|
|
392
|
+
exe: fake.exe,
|
|
393
|
+
provider: 'agnes-ai',
|
|
394
|
+
keys: ['sk-agbad-0', 'sk-aggood-1'],
|
|
395
|
+
extraArgs: ['--fake-fail-key=sk-agbad'],
|
|
396
|
+
},
|
|
397
|
+
{ id: 'g-cl', kind: 'claude', exe: fake.exe },
|
|
398
|
+
],
|
|
399
|
+
})
|
|
400
|
+
let r = [
|
|
401
|
+
t.ok,
|
|
402
|
+
t.providerId,
|
|
403
|
+
t.keyIndex,
|
|
404
|
+
getInjectedKey(t.stdout),
|
|
405
|
+
t.tried.map((x) => [x.keyId, x.outcome]),
|
|
406
|
+
]
|
|
407
|
+
let rr = [
|
|
408
|
+
true,
|
|
409
|
+
'g-ag',
|
|
410
|
+
1,
|
|
411
|
+
'sk-aggood-1',
|
|
412
|
+
[
|
|
413
|
+
['g-ds#0', 'next-key'],
|
|
414
|
+
['g-ds#1', 'next-key'],
|
|
415
|
+
['g-ag#0', 'next-key'],
|
|
416
|
+
['g-ag#1', 'ok'],
|
|
417
|
+
],
|
|
418
|
+
]
|
|
419
|
+
assert.strict.deepEqual(r, rr)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import dispatchAntigravity from '../src/dispatchAntigravity.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('dispatchAntigravity', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
|
|
10
|
+
before(function() {
|
|
11
|
+
fake = createFakeCli('fake-agy')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(function() {
|
|
15
|
+
if (fake) {
|
|
16
|
+
fake.clean()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('prompt非有效字串時回傳錯誤結果物件且不reject', async function() {
|
|
21
|
+
let r = []
|
|
22
|
+
for (let prompt of [null, undefined, '', 123, {}, []]) {
|
|
23
|
+
let t = await dispatchAntigravity(prompt)
|
|
24
|
+
r.push([t.ok, t.error, t.attempts])
|
|
25
|
+
}
|
|
26
|
+
let rr = [
|
|
27
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
28
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
29
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
30
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
31
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
32
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
33
|
+
]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('prompt超過30000字元時回傳prompt too long且不spawn', async function() {
|
|
38
|
+
let t = await dispatchAntigravity('x'.repeat(30001), { exe: fake.exe })
|
|
39
|
+
let r = [t.ok, t.error.indexOf('prompt too long (30001 chars > 30000)') === 0, t.attempts]
|
|
40
|
+
let rr = [false, true, 0]
|
|
41
|
+
assert.strict.deepEqual(r, rr)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('prompt恰為30000字元時可正常spawn', async function() {
|
|
45
|
+
let prompt = 'x'.repeat(30000)
|
|
46
|
+
let t = await dispatchAntigravity(prompt, { exe: fake.exe })
|
|
47
|
+
let o = JSON.parse(t.stdout)
|
|
48
|
+
let r = [t.ok, o.args[o.args.length - 1] === prompt]
|
|
49
|
+
let rr = [true, true]
|
|
50
|
+
assert.strict.deepEqual(r, rr)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('轉義膨脹突破命令列上限時由try/catch兜底回傳錯誤結果物件不reject', async function() {
|
|
54
|
+
//29000個引號長度過前置檢查, 但spawn轉義後約58000字元 → 同步拋ENAMETOOLONG
|
|
55
|
+
let t = await dispatchAntigravity('"'.repeat(29000), { exe: fake.exe })
|
|
56
|
+
let r = [t.ok, t.error.includes('ENAMETOOLONG'), t.attempts]
|
|
57
|
+
let rr = [false, true, 0]
|
|
58
|
+
assert.strict.deepEqual(r, rr)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('預設帶--dangerously-skip-permissions與--print-timeout, prompt為--print之值且位於最後', async function() {
|
|
62
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe })
|
|
63
|
+
let o = JSON.parse(t.stdout)
|
|
64
|
+
let r = [t.ok, o.args]
|
|
65
|
+
let rr = [true, ['--dangerously-skip-permissions', '--print-timeout', '270s', '--print', 'abc']]
|
|
66
|
+
assert.strict.deepEqual(r, rr)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('prompt不走stdin(agy由--print取得, 塞stdin會進互動模式)', async function() {
|
|
70
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe })
|
|
71
|
+
let o = JSON.parse(t.stdout)
|
|
72
|
+
let r = [o.stdin, o.args.includes('abc')]
|
|
73
|
+
let rr = ['', true]
|
|
74
|
+
assert.strict.deepEqual(r, rr)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('有給model時帶--model旗標', async function() {
|
|
78
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, model: 'gemini-3.6-flash-low' })
|
|
79
|
+
let o = JSON.parse(t.stdout)
|
|
80
|
+
let r = o.args
|
|
81
|
+
let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'gemini-3.6-flash-low', '--print', 'abc']
|
|
82
|
+
assert.strict.deepEqual(r, rr)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('有給effort時帶--effort旗標', async function() {
|
|
86
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, model: 'gemini-3.1-pro', effort: 'low' })
|
|
87
|
+
let o = JSON.parse(t.stdout)
|
|
88
|
+
let r = o.args
|
|
89
|
+
let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--model', 'gemini-3.1-pro', '--effort', 'low', '--print', 'abc']
|
|
90
|
+
assert.strict.deepEqual(r, rr)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('skipPermissions為false時不帶--dangerously-skip-permissions旗標', async function() {
|
|
94
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, skipPermissions: false })
|
|
95
|
+
let o = JSON.parse(t.stdout)
|
|
96
|
+
let r = [t.ok, o.args]
|
|
97
|
+
let rr = [true, ['--print-timeout', '270s', '--print', 'abc']]
|
|
98
|
+
assert.strict.deepEqual(r, rr)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('printTimeout由timeoutMs推導並預留30秒緩衝, 下限30秒', async function() {
|
|
102
|
+
let r = []
|
|
103
|
+
for (let timeoutMs of [600000, 120000, 5000, undefined]) {
|
|
104
|
+
let opt = { exe: fake.exe }
|
|
105
|
+
if (timeoutMs !== undefined) {
|
|
106
|
+
opt.timeoutMs = timeoutMs
|
|
107
|
+
}
|
|
108
|
+
let t = await dispatchAntigravity('abc', opt)
|
|
109
|
+
let o = JSON.parse(t.stdout)
|
|
110
|
+
let i = o.args.indexOf('--print-timeout')
|
|
111
|
+
r.push(o.args[i + 1])
|
|
112
|
+
}
|
|
113
|
+
let rr = ['570s', '90s', '30s', '270s'] //600-30, 120-30, max(30,5-30), 預設300-30
|
|
114
|
+
assert.strict.deepEqual(r, rr)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('printTimeout明確給定時不推導', async function() {
|
|
118
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, printTimeout: '10m', timeoutMs: 600000 })
|
|
119
|
+
let o = JSON.parse(t.stdout)
|
|
120
|
+
let i = o.args.indexOf('--print-timeout')
|
|
121
|
+
let r = o.args[i + 1]
|
|
122
|
+
let rr = '10m'
|
|
123
|
+
assert.strict.deepEqual(r, rr)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('addDirs逐項展開為--add-dir並濾除非有效字串', async function() {
|
|
127
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, addDirs: ['c:/a', null, 'c:/b', 123] })
|
|
128
|
+
let o = JSON.parse(t.stdout)
|
|
129
|
+
let r = o.args
|
|
130
|
+
let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--add-dir', 'c:/a', '--add-dir', 'c:/b', '--print', 'abc']
|
|
131
|
+
assert.strict.deepEqual(r, rr)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('extraArgs接於固定旗標之後且於--print之前', async function() {
|
|
135
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, extraArgs: ['--output-format', 'json'] })
|
|
136
|
+
let o = JSON.parse(t.stdout)
|
|
137
|
+
let r = o.args
|
|
138
|
+
let rr = ['--dangerously-skip-permissions', '--print-timeout', '270s', '--output-format', 'json', '--print', 'abc']
|
|
139
|
+
assert.strict.deepEqual(r, rr)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('中文與多行prompt於--print值內完整保留', async function() {
|
|
143
|
+
let prompt = '第一行中文\n第二行 with "quote" & <sym>\n第三行'
|
|
144
|
+
let t = await dispatchAntigravity(prompt, { exe: fake.exe })
|
|
145
|
+
let o = JSON.parse(t.stdout)
|
|
146
|
+
let r = o.args[o.args.length - 1] === prompt
|
|
147
|
+
let rr = true
|
|
148
|
+
assert.strict.deepEqual(r, rr)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('執行檔不存在時回傳ENOENT且不重試', async function() {
|
|
152
|
+
let t = await dispatchAntigravity('abc', { exe: 'agy-not-exist-for-test', maxRetries: 3 })
|
|
153
|
+
let r = [t.ok, t.error.includes('ENOENT'), t.attempts]
|
|
154
|
+
let rr = [false, true, 1]
|
|
155
|
+
assert.strict.deepEqual(r, rr)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('離開碼非0時回傳失敗(如model與effort衝突時agy回exit 1)', async function() {
|
|
159
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, extraArgs: ['--fake-exit=1', '--fake-stderr=conflicts with --effort'] })
|
|
160
|
+
let r = [t.ok, t.code, t.error, t.stderr]
|
|
161
|
+
let rr = [false, 1, 'Exit code 1', 'conflicts with --effort']
|
|
162
|
+
assert.strict.deepEqual(r, rr)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('cwd等execCli設定原樣轉傳', async function() {
|
|
166
|
+
let t = await dispatchAntigravity('abc', { exe: fake.exe, cwd: process.cwd() })
|
|
167
|
+
let o = JSON.parse(t.stdout)
|
|
168
|
+
let r = [t.ok, o.cwd === process.cwd()]
|
|
169
|
+
let rr = [true, true]
|
|
170
|
+
assert.strict.deepEqual(r, rr)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
})
|