w-dispatch-ai 1.0.7 → 1.0.9
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 +38 -1
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +4 -3
- package/docs/adapters.mjs.html +2 -2
- package/docs/castPintOr.mjs.html +109 -0
- package/docs/dfTimeoutMs.mjs.html +2 -2
- package/docs/dispatchAi.mjs.html +14 -13
- package/docs/dispatchAiFallback.mjs.html +156 -97
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +10 -14
- package/docs/dispatchApiOpenaiCompat.mjs.html +50 -64
- package/docs/dispatchClaude.mjs.html +8 -13
- package/docs/dispatchCodex.mjs.html +8 -13
- package/docs/dispatchOpencode.mjs.html +8 -13
- package/docs/getCliArgs.mjs.html +2 -2
- package/docs/getErrorResult.mjs.html +12 -5
- package/docs/getErrorType.mjs.html +171 -0
- package/docs/global.html +1275 -88
- package/docs/index.html +2 -2
- package/docs/resolveProviders.mjs.html +2 -2
- package/docs/wkf_callAiWithFallback.mjs.html +14 -10
- package/docs/wkf_extractJsonLoose.mjs.html +2 -2
- package/docs/wkf_runFanout.mjs.html +8 -8
- package/docs/wkf_runFanoutPipeline.mjs.html +14 -18
- package/docs/wkf_runRolePipeline.mjs.html +5 -4
- package/package.json +1 -1
- package/src/WDispatchAi.mjs +2 -1
- package/src/castPintOr.mjs +37 -0
- package/src/dispatchAi.mjs +12 -11
- package/src/dispatchAiFallback.mjs +154 -95
- package/src/dispatchAntigravity.mjs +8 -12
- package/src/dispatchApiOpenaiCompat.mjs +48 -62
- package/src/dispatchClaude.mjs +6 -11
- package/src/dispatchCodex.mjs +6 -11
- package/src/dispatchOpencode.mjs +6 -11
- package/src/getErrorResult.mjs +10 -3
- package/src/getErrorType.mjs +99 -0
- package/src/providers.mjs +5 -0
- package/src/wkf/callAiWithFallback.mjs +12 -8
- package/src/wkf/runFanout.mjs +6 -6
- package/src/wkf/runFanoutPipeline.mjs +12 -16
- package/src/wkf/runRolePipeline.mjs +3 -2
- package/test/tools/fakeServerForApiTest.mjs +4 -1
- package/test/unit-callAiWithFallback.test.mjs +24 -0
- package/test/unit-castPintOr.test.mjs +28 -0
- package/test/unit-dispatchAiFallback.test.mjs +137 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +39 -0
- package/test/unit-getErrorResult.test.mjs +13 -1
- package/test/unit-getErrorType.test.mjs +36 -0
- package/test/unit-providers.test.mjs +24 -0
- package/test/unit-runFanout.test.mjs +14 -0
- package/test/unit-runRolePipeline.test.mjs +12 -0
|
@@ -539,4 +539,141 @@ describe('dispatchAiFallback', function() {
|
|
|
539
539
|
assert.strict.deepEqual(r, rr)
|
|
540
540
|
})
|
|
541
541
|
|
|
542
|
+
it('shouldStop於嘗試邊界檢查, true即停止遞補回報ABORTED', async function() {
|
|
543
|
+
//首次嘗試邊界放行(n=0), 第二次嘗試邊界(st-a已敗轉st-b前)即中止——
|
|
544
|
+
//「斷線後仍空耗整條鏈」縮成「至多再耗當前這一家」
|
|
545
|
+
let n = 0
|
|
546
|
+
let t = await dispatchAiFallback('abc', {
|
|
547
|
+
providers: [
|
|
548
|
+
{ id: 'st-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
549
|
+
{ id: 'st-b', kind: 'claude', exe: fake.exe },
|
|
550
|
+
],
|
|
551
|
+
shouldStop: () => n++ >= 1,
|
|
552
|
+
})
|
|
553
|
+
let r = [t.ok, t.error, t.tried.map((x) => [x.providerId, x.outcome])]
|
|
554
|
+
let rr = [false, 'ABORTED', [['st-a', 'next-key'], ['st-b', 'aborted']]]
|
|
555
|
+
assert.strict.deepEqual(r, rr)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('shouldStop一開始即true時完全不嘗試; 回調拋出例外視同false不中止', async function() {
|
|
559
|
+
let t1 = await dispatchAiFallback('abc', {
|
|
560
|
+
providers: [{ id: 'st2-a', kind: 'claude', exe: fake.exe }],
|
|
561
|
+
shouldStop: () => true,
|
|
562
|
+
})
|
|
563
|
+
let t2 = await dispatchAiFallback('abc', {
|
|
564
|
+
providers: [{ id: 'st2-a', kind: 'claude', exe: fake.exe }],
|
|
565
|
+
shouldStop: () => {
|
|
566
|
+
throw new Error('callback error should not break the loop')
|
|
567
|
+
},
|
|
568
|
+
})
|
|
569
|
+
let r = [[t1.ok, t1.error, t1.tried.map((x) => x.outcome)], [t2.ok, t2.error]]
|
|
570
|
+
let rr = [[false, 'ABORTED', ['aborted']], [true, '']]
|
|
571
|
+
assert.strict.deepEqual(r, rr)
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('coolDetect注入CLI限流簽章判定, 命中即觸發冷卻次輪降尾', async function() {
|
|
575
|
+
let stored = { cursors: {}, cooling: {} }
|
|
576
|
+
let store = {
|
|
577
|
+
get: () => stored,
|
|
578
|
+
set: (s) => {
|
|
579
|
+
stored = s
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
//CLI限流埋在stderr(如Zen之FreeUsageLimitError), 內建429/TIMEOUT觸發不到,
|
|
583
|
+
//簽章由呼叫端以coolDetect注入判定
|
|
584
|
+
let pLimited = { id: 'cdx-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1', '--fake-stderr=FreeUsageLimitError: quota exceeded'] }
|
|
585
|
+
let pOk = { id: 'cdx-b', kind: 'claude', exe: fake.exe }
|
|
586
|
+
let coolDetect = (r) => /FreeUsageLimitError/i.test(r.stderr || '')
|
|
587
|
+
let r1 = await dispatchAiFallback('abc', { providers: [pLimited, pOk], store, cooldownMs: 300000, coolDetect })
|
|
588
|
+
let r2 = await dispatchAiFallback('abc', { providers: [pLimited, pOk], store, cooldownMs: 300000, coolDetect })
|
|
589
|
+
let r = [
|
|
590
|
+
r1.providerId,
|
|
591
|
+
typeof stored.cooling['cdx-a'],
|
|
592
|
+
r2.providerId,
|
|
593
|
+
r2.tried.map((x) => x.providerId), //次輪cdx-a降尾, 完全未被嘗試
|
|
594
|
+
]
|
|
595
|
+
let rr = ['cdx-b', 'number', 'cdx-b', ['cdx-b']]
|
|
596
|
+
assert.strict.deepEqual(r, rr)
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('coolDetect拋出例外視同false不觸發冷卻; meta為保留鍵不影響呼叫行為', async function() {
|
|
600
|
+
let stored = { cursors: {}, cooling: {} }
|
|
601
|
+
let store = {
|
|
602
|
+
get: () => stored,
|
|
603
|
+
set: (s) => {
|
|
604
|
+
stored = s
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
let t1 = await dispatchAiFallback('abc', {
|
|
608
|
+
providers: [
|
|
609
|
+
{ id: 'cde-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
610
|
+
{ id: 'cde-b', kind: 'claude', exe: fake.exe },
|
|
611
|
+
],
|
|
612
|
+
store,
|
|
613
|
+
cooldownMs: 300000,
|
|
614
|
+
coolDetect: () => {
|
|
615
|
+
throw new Error('callback error should not break the loop')
|
|
616
|
+
},
|
|
617
|
+
})
|
|
618
|
+
//條目與opt掛meta(保留鍵)不轉傳: 假CLI收到之args與stdin與未掛時完全一致
|
|
619
|
+
let t2 = await dispatchAiFallback('abc', {
|
|
620
|
+
providers: [{ id: 'mt-a', kind: 'claude', exe: fake.exe, model: 'sonnet', meta: { tag: 'draft' } }],
|
|
621
|
+
meta: { batch: 1 },
|
|
622
|
+
})
|
|
623
|
+
let o = JSON.parse(t2.stdout)
|
|
624
|
+
let r = [t1.ok, stored.cooling['cde-a'], t2.ok, o.args, o.stdin]
|
|
625
|
+
let rr = [true, undefined, true, ['-p', '--dangerously-skip-permissions', '--model', 'sonnet'], 'abc']
|
|
626
|
+
assert.strict.deepEqual(r, rr)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('cooled事件於冷卻觸發時發出, tried與失敗事件帶機器可讀errorType', async function() {
|
|
630
|
+
let evs = []
|
|
631
|
+
let stored = { cursors: {}, cooling: {} }
|
|
632
|
+
let store = {
|
|
633
|
+
get: () => stored,
|
|
634
|
+
set: (s) => {
|
|
635
|
+
stored = s
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
let t = await dispatchAiFallback('abc', {
|
|
639
|
+
providers: [
|
|
640
|
+
{ id: 'ev-hang', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-sleep=9000'], timeoutMs: 500 }, //逾時 → 冷卻觸發
|
|
641
|
+
{ id: 'ev-ok', kind: 'claude', exe: fake.exe },
|
|
642
|
+
],
|
|
643
|
+
store,
|
|
644
|
+
cooldownMs: 300000,
|
|
645
|
+
onEvent: (ev) => evs.push(ev),
|
|
646
|
+
})
|
|
647
|
+
let evCooled = evs.find((x) => x.type === 'cooled')
|
|
648
|
+
let evSkip = evs.find((x) => x.type === 'skip-group')
|
|
649
|
+
let r = [
|
|
650
|
+
t.ok,
|
|
651
|
+
evCooled.providerId,
|
|
652
|
+
evCooled.cooldownMs,
|
|
653
|
+
evCooled.error.indexOf('TIMEOUT') === 0,
|
|
654
|
+
evSkip.errorType, //失敗事件帶機器可讀分類
|
|
655
|
+
t.tried[0].errorType, //tried歷程各項一併帶上
|
|
656
|
+
]
|
|
657
|
+
let rr = [true, 'ev-hang', 300000, true, 'timeout', 'timeout']
|
|
658
|
+
assert.strict.deepEqual(r, rr)
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
it('最終失敗結果帶errorType: 一般執行失敗exec, 中止aborted, 預算用盡budget', async function() {
|
|
662
|
+
let t1 = await dispatchAiFallback('abc', {
|
|
663
|
+
providers: [{ id: 'et-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] }],
|
|
664
|
+
})
|
|
665
|
+
let t2 = await dispatchAiFallback('abc', {
|
|
666
|
+
providers: [{ id: 'et-b', kind: 'claude', exe: fake.exe }],
|
|
667
|
+
shouldStop: () => true,
|
|
668
|
+
})
|
|
669
|
+
let t3 = await dispatchAiFallback('abc', {
|
|
670
|
+
providers: [{ id: 'et-c', kind: 'claude', exe: fake.exe }],
|
|
671
|
+
budgetMs: 60000,
|
|
672
|
+
minAttemptMs: 600000,
|
|
673
|
+
})
|
|
674
|
+
let r = [[t1.ok, t1.errorType], [t2.ok, t2.errorType], [t3.ok, t3.errorType]]
|
|
675
|
+
let rr = [[false, 'exec'], [false, 'aborted'], [false, 'budget']]
|
|
676
|
+
assert.strict.deepEqual(r, rr)
|
|
677
|
+
})
|
|
678
|
+
|
|
542
679
|
})
|
|
@@ -289,4 +289,43 @@ describe('dispatchApiOpenaiCompat', function() {
|
|
|
289
289
|
assert.strict.deepEqual(r, rr)
|
|
290
290
|
})
|
|
291
291
|
|
|
292
|
+
it('usage原樣透傳: 成功帶回應之token用量, 網路層與HTTP錯誤為null', async function() {
|
|
293
|
+
let t1 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo' })
|
|
294
|
+
let t2 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'err-500' })
|
|
295
|
+
let r = [t1.ok, t1.usage, t2.ok, t2.usage]
|
|
296
|
+
let rr = [true, { prompt_tokens: 3, completion_tokens: 7, total_tokens: 10 }, false, null]
|
|
297
|
+
assert.strict.deepEqual(r, rr)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('usage經遞補層與工作流層流出於結果與tried, CLI路徑無此欄', async function() {
|
|
301
|
+
let tf = await dispatchAiFallback('abc', {
|
|
302
|
+
providers: [{ id: 'us-api', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-good-us'] }],
|
|
303
|
+
})
|
|
304
|
+
let wkf = dispatchAiWkf({
|
|
305
|
+
providers: {
|
|
306
|
+
'us-w-api': { kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-good-us2'] },
|
|
307
|
+
},
|
|
308
|
+
defaults: { promptPrefix: '' },
|
|
309
|
+
})
|
|
310
|
+
let tw = await wkf.callAi('abc', { spec: { use: 'us-w-api' } })
|
|
311
|
+
let r = [
|
|
312
|
+
tf.usage.total_tokens,
|
|
313
|
+
tf.tried[0].usage.total_tokens, //tried歷程各項一併帶上, 供加總實際耗用
|
|
314
|
+
tw.usage.total_tokens,
|
|
315
|
+
]
|
|
316
|
+
let rr = [10, 10, 10]
|
|
317
|
+
assert.strict.deepEqual(r, rr)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('errorType機器可讀分類: http/tool-unsupported/validation/timeout, 成功無此欄', async function() {
|
|
321
|
+
let t1 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'err-500' })
|
|
322
|
+
let t2 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'tool-calls' })
|
|
323
|
+
let t3 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo', validate: 'min:100000' })
|
|
324
|
+
let t4 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'slow', timeoutMs: 500 })
|
|
325
|
+
let t5 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo' })
|
|
326
|
+
let r = [t1.errorType, t2.errorType, t3.errorType, t4.errorType, t5.errorType]
|
|
327
|
+
let rr = ['http', 'tool-unsupported', 'validation', 'timeout', undefined]
|
|
328
|
+
assert.strict.deepEqual(r, rr)
|
|
329
|
+
})
|
|
330
|
+
|
|
292
331
|
})
|
|
@@ -12,6 +12,7 @@ describe('getErrorResult', function() {
|
|
|
12
12
|
stderr: '',
|
|
13
13
|
code: null,
|
|
14
14
|
error: 'something wrong',
|
|
15
|
+
errorType: 'params',
|
|
15
16
|
durationMs: 0,
|
|
16
17
|
attempts: 0,
|
|
17
18
|
}
|
|
@@ -20,7 +21,18 @@ describe('getErrorResult', function() {
|
|
|
20
21
|
|
|
21
22
|
it('鍵名與順序固定', function() {
|
|
22
23
|
let r = Object.keys(getErrorResult('abc'))
|
|
23
|
-
let rr = ['ok', 'stdout', 'stderr', 'code', 'error', 'durationMs', 'attempts']
|
|
24
|
+
let rr = ['ok', 'stdout', 'stderr', 'code', 'error', 'errorType', 'durationMs', 'attempts']
|
|
25
|
+
assert.strict.deepEqual(r, rr)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('errorType可指定, 非有效字串回退params', function() {
|
|
29
|
+
let r = [
|
|
30
|
+
getErrorResult('x').errorType,
|
|
31
|
+
getErrorResult('x', 'aborted').errorType,
|
|
32
|
+
getErrorResult('x', null).errorType,
|
|
33
|
+
getErrorResult('x', '').errorType,
|
|
34
|
+
]
|
|
35
|
+
let rr = ['params', 'aborted', 'params', 'params']
|
|
24
36
|
assert.strict.deepEqual(r, rr)
|
|
25
37
|
})
|
|
26
38
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import getErrorType, { attachErrorType } from '../src/getErrorType.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('getErrorType', function() {
|
|
6
|
+
|
|
7
|
+
it('機械可判之類別: timeout/spawn/validation, 其餘歸exec', function() {
|
|
8
|
+
let r = [
|
|
9
|
+
getErrorType({ error: 'TIMEOUT after 300s' }),
|
|
10
|
+
getErrorType({ error: 'spawn my-cli ENOENT' }),
|
|
11
|
+
getErrorType({ error: 'ENAMETOOLONG: spawn error' }),
|
|
12
|
+
getErrorType({ error: 'OUTPUT_VALIDATION_FAILED' }),
|
|
13
|
+
getErrorType({ error: 'Exit code 1' }),
|
|
14
|
+
getErrorType({ error: 'Invalid API key.' }),
|
|
15
|
+
getErrorType({}),
|
|
16
|
+
getErrorType({ error: '' }),
|
|
17
|
+
]
|
|
18
|
+
let rr = ['timeout', 'spawn', 'spawn', 'validation', 'exec', 'exec', 'exec', 'exec']
|
|
19
|
+
assert.strict.deepEqual(r, rr)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('attachErrorType僅對失敗且未帶errorType之結果追加', function() {
|
|
23
|
+
let rOk = { ok: true, stdout: 'abc' }
|
|
24
|
+
let rFail = { ok: false, error: 'TIMEOUT after 10s' }
|
|
25
|
+
let rTyped = { ok: false, error: 'HTTP 429', errorType: 'http' }
|
|
26
|
+
let r = [
|
|
27
|
+
attachErrorType(rOk).errorType,
|
|
28
|
+
attachErrorType(rOk) === rOk, //成功結果原樣回傳
|
|
29
|
+
attachErrorType(rFail).errorType,
|
|
30
|
+
attachErrorType(rTyped).errorType, //已帶有效errorType則不覆寫
|
|
31
|
+
]
|
|
32
|
+
let rr = [undefined, true, 'timeout', 'http']
|
|
33
|
+
assert.strict.deepEqual(r, rr)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
})
|
|
@@ -63,4 +63,28 @@ describe('providers', function() {
|
|
|
63
63
|
assert.strict.deepEqual(r, rr)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
+
it('CLI條目皆自帶機械防寫(內建清單定位為唯讀調用, 新增CLI條目時必附防寫欄位)', function() {
|
|
67
|
+
//各kind之防寫機制不同(對照表見README); 曾因agy條目獨漏防寫,
|
|
68
|
+
//照內建清單原樣使用時canary實測要求建檔會真的落地(2026-08-15使用端回報+本機復測)
|
|
69
|
+
let hasLock = (p) => {
|
|
70
|
+
if (p.kind === 'opencode') {
|
|
71
|
+
let perm = p.config && p.config.permission
|
|
72
|
+
return !!perm && perm.edit === 'deny' && perm.write === 'deny' && perm.bash === 'deny'
|
|
73
|
+
}
|
|
74
|
+
if (p.kind === 'claude') {
|
|
75
|
+
return Array.isArray(p.extraArgs) && p.extraArgs.includes('--disallowedTools')
|
|
76
|
+
}
|
|
77
|
+
if (p.kind === 'codex') {
|
|
78
|
+
return p.sandbox === 'read-only'
|
|
79
|
+
}
|
|
80
|
+
if (p.kind === 'antigravity') {
|
|
81
|
+
return p.skipPermissions === false //保留agy權限閘門
|
|
82
|
+
}
|
|
83
|
+
return true //api類純文字生成天然無寫檔能力
|
|
84
|
+
}
|
|
85
|
+
let r = providers.map((p) => [p.id, hasLock(p)])
|
|
86
|
+
let rr = providers.map((p) => [p.id, true])
|
|
87
|
+
assert.strict.deepEqual(r, rr)
|
|
88
|
+
})
|
|
89
|
+
|
|
66
90
|
})
|
|
@@ -202,6 +202,20 @@ describe('runFanout', function() {
|
|
|
202
202
|
assert.strict.deepEqual(r, rr)
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
+
it('meta為保留鍵: 名額與integrate規格掛meta不影響呼叫行為', async function() {
|
|
206
|
+
let t = await runFanout({
|
|
207
|
+
providers,
|
|
208
|
+
task: 'abc',
|
|
209
|
+
agents: [{ use: 'p-a', meta: { stage: 'draft' } }],
|
|
210
|
+
integrate: { use: 'p-int', meta: { stage: 'final' } },
|
|
211
|
+
callOpt,
|
|
212
|
+
})
|
|
213
|
+
//單稿放行, 假CLI收到之args與未掛meta時完全一致(meta未被轉傳成未知旗標)
|
|
214
|
+
let r = [t.ok, t.integrated, t.result.args]
|
|
215
|
+
let rr = [true, false, ['-p', '--dangerously-skip-permissions', '--model', 'sonnet']]
|
|
216
|
+
assert.strict.deepEqual(r, rr)
|
|
217
|
+
})
|
|
218
|
+
|
|
205
219
|
it('defaultIntegratePrompt嵌入候選數與schema', function() {
|
|
206
220
|
let s = defaultIntegratePrompt([{ a: 1 }, { b: 2 }], { schema: '{"x":"..."}' })
|
|
207
221
|
let r = [s.includes('2 個獨立執行'), s.includes('{"x":"..."}'), s.includes('【候選 2】')]
|
|
@@ -160,4 +160,16 @@ describe('runRolePipeline', function() {
|
|
|
160
160
|
assert.strict.deepEqual(r, rr)
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
+
it('meta為保留鍵: 階段規格掛meta(如draft/audit分類)不影響呼叫行為', async function() {
|
|
164
|
+
let t = await runRolePipeline({
|
|
165
|
+
providers,
|
|
166
|
+
stages: [{ id: 's1', use: 'p-a', prompt: () => 'abc', meta: { stage: 'draft' } }],
|
|
167
|
+
callOpt,
|
|
168
|
+
})
|
|
169
|
+
//假CLI收到之args與未掛meta時完全一致(meta未被轉傳成未知旗標)
|
|
170
|
+
let r = [t.ok, t.result.args, t.result.stdin]
|
|
171
|
+
let rr = [true, ['-p', '--dangerously-skip-permissions', '--model', 'sonnet'], 'abc']
|
|
172
|
+
assert.strict.deepEqual(r, rr)
|
|
173
|
+
})
|
|
174
|
+
|
|
163
175
|
})
|