w-dispatch-ai 1.0.2 → 1.0.4
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 +123 -35
- 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 +43 -7
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +42 -8
- package/docs/dispatchAiWkf.mjs.html +197 -0
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +516 -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 +5768 -1469
- package/docs/index.html +2 -2
- package/docs/wkf_callAiWithFallback.mjs.html +282 -0
- package/docs/wkf_extractJsonLoose.mjs.html +180 -0
- package/docs/wkf_runFanout.mjs.html +227 -0
- package/docs/wkf_runFanoutPipeline.mjs.html +178 -0
- package/docs/wkf_runRolePipeline.mjs.html +195 -0
- package/g.mjs +41 -25
- package/package.json +1 -1
- package/src/WDispatchAi.mjs +6 -2
- package/src/adapters.mjs +41 -5
- package/src/dispatchAiFallback.mjs +40 -6
- package/src/dispatchAiWkf.mjs +125 -0
- package/src/dispatchApiOpenaiCompat.mjs +444 -0
- package/src/wkf/callAiWithFallback.mjs +210 -0
- package/src/wkf/extractJsonLoose.mjs +108 -0
- package/src/wkf/runFanout.mjs +155 -0
- package/src/wkf/runFanoutPipeline.mjs +106 -0
- package/src/wkf/runRolePipeline.mjs +123 -0
- package/test/tools/fakeServerForApiTest.mjs +151 -0
- package/test/unit-WDispatchAi.test.mjs +13 -6
- package/test/unit-adapters.test.mjs +5 -3
- package/test/unit-callAiWithFallback.test.mjs +146 -0
- package/test/unit-dispatchAi.test.mjs +1 -1
- package/test/unit-dispatchAiWkf.test.mjs +118 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +264 -0
- package/test/unit-extractJsonLoose.test.mjs +78 -0
- package/test/unit-runFanout.test.mjs +166 -0
- package/test/unit-runFanoutPipeline.test.mjs +86 -0
- package/test/unit-runRolePipeline.test.mjs +163 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import extractJsonLoose from '../src/wkf/extractJsonLoose.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('extractJsonLoose', function() {
|
|
6
|
+
|
|
7
|
+
it('整段即為合法JSON時直接解析', function() {
|
|
8
|
+
let r = [
|
|
9
|
+
extractJsonLoose('{"a":1}'),
|
|
10
|
+
extractJsonLoose('[1,2,3]'),
|
|
11
|
+
extractJsonLoose(' {"a":{"b":[1]}} '),
|
|
12
|
+
]
|
|
13
|
+
let rr = [{ a: 1 }, [1, 2, 3], { a: { b: [1] } }]
|
|
14
|
+
assert.strict.deepEqual(r, rr)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('去除code fence與前後說明文字後抽取第一個完整物件', function() {
|
|
18
|
+
let r = [
|
|
19
|
+
extractJsonLoose('說明文字\n```json\n{"a":1}\n```\n後記'),
|
|
20
|
+
extractJsonLoose('```\n{"a":1}\n```'),
|
|
21
|
+
extractJsonLoose('前綴 {"a":1} 後綴 {"b":2}'),
|
|
22
|
+
]
|
|
23
|
+
let rr = [{ a: 1 }, { a: 1 }, { a: 1 }]
|
|
24
|
+
assert.strict.deepEqual(r, rr)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('去除ANSI色碼', function() {
|
|
28
|
+
let esc = String.fromCharCode(27)
|
|
29
|
+
let r = extractJsonLoose(`${esc}[0m{"a":1}${esc}[91m`)
|
|
30
|
+
let rr = { a: 1 }
|
|
31
|
+
assert.strict.deepEqual(r, rr)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('字串值內之括號與跳脫不干擾配對', function() {
|
|
35
|
+
let r = extractJsonLoose('{"a":"x{y}[z]","b":"q\\"w"}')
|
|
36
|
+
let rr = { a: 'x{y}[z]', b: 'q"w' }
|
|
37
|
+
assert.strict.deepEqual(r, rr)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('截斷(括號未閉合)回傳null', function() {
|
|
41
|
+
let r = [
|
|
42
|
+
extractJsonLoose('{"a":1'),
|
|
43
|
+
extractJsonLoose('[1,2'),
|
|
44
|
+
extractJsonLoose('{"a":"未閉合字串'),
|
|
45
|
+
]
|
|
46
|
+
let rr = [null, null, null]
|
|
47
|
+
assert.strict.deepEqual(r, rr)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('非物件與陣列之JSON回傳null', function() {
|
|
51
|
+
let r = [
|
|
52
|
+
extractJsonLoose('"hello"'),
|
|
53
|
+
extractJsonLoose('123'),
|
|
54
|
+
extractJsonLoose('true'),
|
|
55
|
+
extractJsonLoose('null'),
|
|
56
|
+
]
|
|
57
|
+
let rr = [null, null, null, null]
|
|
58
|
+
assert.strict.deepEqual(r, rr)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('無JSON內容回傳null', function() {
|
|
62
|
+
let r = [
|
|
63
|
+
extractJsonLoose('純文字回覆'),
|
|
64
|
+
extractJsonLoose(''),
|
|
65
|
+
extractJsonLoose(null),
|
|
66
|
+
extractJsonLoose(undefined),
|
|
67
|
+
]
|
|
68
|
+
let rr = [null, null, null, null]
|
|
69
|
+
assert.strict.deepEqual(r, rr)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('第一個片段非法時回傳null(不繼續嘗試後續片段)', function() {
|
|
73
|
+
let r = extractJsonLoose('{"a":1,} {"b":2}')
|
|
74
|
+
let rr = null
|
|
75
|
+
assert.strict.deepEqual(r, rr)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import runFanout, { defaultIntegratePrompt } from '../src/wkf/runFanout.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('runFanout', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
let providers = null
|
|
10
|
+
let callOpt = { promptPrefix: '' }
|
|
11
|
+
|
|
12
|
+
before(function() {
|
|
13
|
+
fake = createFakeCli('fake-wkf-fanout')
|
|
14
|
+
providers = {
|
|
15
|
+
'p-a': { kind: 'claude', exe: fake.exe, model: 'sonnet' },
|
|
16
|
+
'p-b': { kind: 'codex', exe: fake.exe, model: 'gpt-5.6-luna' },
|
|
17
|
+
'p-int': { kind: 'opencode', exe: fake.exe, model: 'opencode/free' },
|
|
18
|
+
'p-dead': { kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
after(function() {
|
|
23
|
+
if (fake) {
|
|
24
|
+
fake.clean()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('task或agents無效時回傳錯誤結果物件', async function() {
|
|
29
|
+
let t1 = await runFanout({ providers, task: '', agents: [{ use: 'p-a' }], callOpt })
|
|
30
|
+
let t2 = await runFanout({ providers, task: 'abc', agents: null, callOpt })
|
|
31
|
+
let r = [[t1.ok, t1.error], [t2.ok, t2.error]]
|
|
32
|
+
let rr = [
|
|
33
|
+
[false, 'task must be a non-empty string'],
|
|
34
|
+
[false, 'agents must be a non-empty array'],
|
|
35
|
+
]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('候選達門檻時進整合, 整合提示詞含各候選', async function() {
|
|
40
|
+
let t = await runFanout({
|
|
41
|
+
providers,
|
|
42
|
+
task: 'abc',
|
|
43
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
44
|
+
integrate: { use: 'p-int' },
|
|
45
|
+
callOpt,
|
|
46
|
+
})
|
|
47
|
+
let r = [
|
|
48
|
+
t.ok,
|
|
49
|
+
t.integrated,
|
|
50
|
+
t.candidates.length,
|
|
51
|
+
t.agents.map((x) => x.providerId),
|
|
52
|
+
t.result.args[0], //整合者為opencode轉接器
|
|
53
|
+
t.result.stdin.includes('【候選 1】') && t.result.stdin.includes('【候選 2】'),
|
|
54
|
+
t.integrateDetail.providerId,
|
|
55
|
+
]
|
|
56
|
+
let rr = [true, true, 2, ['p-a', 'p-b'], 'run', true, 'p-int']
|
|
57
|
+
assert.strict.deepEqual(r, rr)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('個別名額失敗不炸整輪, 恰1份成功候選直接放行(integrated:false)', async function() {
|
|
61
|
+
let t = await runFanout({
|
|
62
|
+
providers,
|
|
63
|
+
task: 'abc',
|
|
64
|
+
agents: [{ use: 'p-dead' }, { use: 'p-a' }],
|
|
65
|
+
integrate: { use: 'p-int' },
|
|
66
|
+
callOpt,
|
|
67
|
+
})
|
|
68
|
+
let r = [t.ok, t.integrated, t.candidates.length, t.result.args, t.agents[0].ok, t.agents[1].ok]
|
|
69
|
+
let rr = [true, false, 1, ['-p', '--dangerously-skip-permissions', '--model', 'sonnet'], false, true]
|
|
70
|
+
assert.strict.deepEqual(r, rr)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('候選未達minCandidates時不硬整合, 以首位候選為成果', async function() {
|
|
74
|
+
let t = await runFanout({
|
|
75
|
+
providers,
|
|
76
|
+
task: 'abc',
|
|
77
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
78
|
+
integrate: { use: 'p-int' },
|
|
79
|
+
minCandidates: 3,
|
|
80
|
+
callOpt,
|
|
81
|
+
})
|
|
82
|
+
let r = [t.ok, t.integrated, t.candidates.length, t.result === t.candidates[0], t.error]
|
|
83
|
+
let rr = [true, false, 2, true, '']
|
|
84
|
+
assert.strict.deepEqual(r, rr)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('全數名額失敗時回傳all agents failed', async function() {
|
|
88
|
+
let t = await runFanout({
|
|
89
|
+
providers,
|
|
90
|
+
task: 'abc',
|
|
91
|
+
agents: [{ use: 'p-dead' }, { use: 'p-dead' }],
|
|
92
|
+
integrate: { use: 'p-int' },
|
|
93
|
+
callOpt,
|
|
94
|
+
})
|
|
95
|
+
let r = [t.ok, t.result, t.candidates.length, t.error]
|
|
96
|
+
let rr = [false, null, 0, 'all agents failed']
|
|
97
|
+
assert.strict.deepEqual(r, rr)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('達門檻但缺integrate規格時回報必要性', async function() {
|
|
101
|
+
let t = await runFanout({
|
|
102
|
+
providers,
|
|
103
|
+
task: 'abc',
|
|
104
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
105
|
+
callOpt,
|
|
106
|
+
})
|
|
107
|
+
let r = [t.ok, t.error, t.candidates.length]
|
|
108
|
+
let rr = [false, 'integrate spec (with use) is required', 2]
|
|
109
|
+
assert.strict.deepEqual(r, rr)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('整合失敗時成功候選仍完整回傳供接續重試', async function() {
|
|
113
|
+
let t = await runFanout({
|
|
114
|
+
providers,
|
|
115
|
+
task: 'abc',
|
|
116
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
117
|
+
integrate: { use: 'p-dead' },
|
|
118
|
+
callOpt,
|
|
119
|
+
})
|
|
120
|
+
let r = [t.ok, t.integrated, t.candidates.length, t.error.indexOf('integrate failed:') === 0]
|
|
121
|
+
let rr = [false, false, 2, true]
|
|
122
|
+
assert.strict.deepEqual(r, rr)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('integrate.prompt可自訂整合提示詞', async function() {
|
|
126
|
+
let seen = null
|
|
127
|
+
let t = await runFanout({
|
|
128
|
+
providers,
|
|
129
|
+
task: 'abc',
|
|
130
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
131
|
+
integrate: {
|
|
132
|
+
use: 'p-int',
|
|
133
|
+
prompt: (candidates) => {
|
|
134
|
+
seen = candidates.length
|
|
135
|
+
return 'CUSTOM-INTEGRATE'
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
callOpt,
|
|
139
|
+
})
|
|
140
|
+
let r = [t.ok, seen, t.result.stdin]
|
|
141
|
+
let rr = [true, 2, 'CUSTOM-INTEGRATE']
|
|
142
|
+
assert.strict.deepEqual(r, rr)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('名額規格之額外鍵覆寫該名額呼叫設定', async function() {
|
|
146
|
+
//p-a名額帶rawText覆寫 → 該名額json為文字, 候選為字串
|
|
147
|
+
let t = await runFanout({
|
|
148
|
+
providers,
|
|
149
|
+
task: 'abc',
|
|
150
|
+
agents: [{ use: 'p-a', rawText: true }],
|
|
151
|
+
integrate: { use: 'p-int' },
|
|
152
|
+
callOpt,
|
|
153
|
+
})
|
|
154
|
+
let r = [t.ok, t.integrated, typeof t.result]
|
|
155
|
+
let rr = [true, false, 'string']
|
|
156
|
+
assert.strict.deepEqual(r, rr)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('defaultIntegratePrompt嵌入候選數與schema', function() {
|
|
160
|
+
let s = defaultIntegratePrompt([{ a: 1 }, { b: 2 }], { schema: '{"x":"..."}' })
|
|
161
|
+
let r = [s.includes('2 個獨立執行'), s.includes('{"x":"..."}'), s.includes('【候選 2】')]
|
|
162
|
+
let rr = [true, true, true]
|
|
163
|
+
assert.strict.deepEqual(r, rr)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import runFanoutPipeline from '../src/wkf/runFanoutPipeline.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('runFanoutPipeline', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
let providers = null
|
|
10
|
+
let callOpt = { promptPrefix: '' }
|
|
11
|
+
|
|
12
|
+
before(function() {
|
|
13
|
+
fake = createFakeCli('fake-wkf-fp')
|
|
14
|
+
providers = {
|
|
15
|
+
'p-a': { kind: 'claude', exe: fake.exe, model: 'sonnet' },
|
|
16
|
+
'p-b': { kind: 'codex', exe: fake.exe, model: 'gpt-5.6-luna' },
|
|
17
|
+
'p-int': { kind: 'opencode', exe: fake.exe, model: 'opencode/free' },
|
|
18
|
+
'p-dead': { kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
after(function() {
|
|
23
|
+
if (fake) {
|
|
24
|
+
fake.clean()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('前段多開整合, 後段以前段成果為input執行角色鏈', async function() {
|
|
29
|
+
let t = await runFanoutPipeline({
|
|
30
|
+
providers,
|
|
31
|
+
task: 'abc',
|
|
32
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
33
|
+
integrate: { use: 'p-int' },
|
|
34
|
+
stages: [
|
|
35
|
+
{ id: 'audit', use: 'p-b', prompt: (ctx) => `audit ${ctx.input.args[0]}` }, //input為整合者echo物件
|
|
36
|
+
],
|
|
37
|
+
callOpt,
|
|
38
|
+
})
|
|
39
|
+
let r = [
|
|
40
|
+
t.ok,
|
|
41
|
+
t.A.ok,
|
|
42
|
+
t.A.integrated,
|
|
43
|
+
t.B.ok,
|
|
44
|
+
t.B.order,
|
|
45
|
+
t.result.stdin, //後段最末階段之echo, 其stdin含前段成果之args[0](整合者為opencode轉接器故為run)
|
|
46
|
+
]
|
|
47
|
+
let rr = [true, true, true, true, ['audit'], 'audit run']
|
|
48
|
+
assert.strict.deepEqual(r, rr)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('前段失敗即回, 後段不執行', async function() {
|
|
52
|
+
let t = await runFanoutPipeline({
|
|
53
|
+
providers,
|
|
54
|
+
task: 'abc',
|
|
55
|
+
agents: [{ use: 'p-dead' }, { use: 'p-dead' }],
|
|
56
|
+
integrate: { use: 'p-int' },
|
|
57
|
+
stages: [{ id: 's1', use: 'p-a', prompt: () => 'x' }],
|
|
58
|
+
callOpt,
|
|
59
|
+
})
|
|
60
|
+
let r = [t.ok, t.A.ok, t.B, t.error.indexOf('A failed:') === 0]
|
|
61
|
+
let rr = [false, false, null, true]
|
|
62
|
+
assert.strict.deepEqual(r, rr)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('後段失敗時前段成果仍完整回傳, 可只重跑後段', async function() {
|
|
66
|
+
let t = await runFanoutPipeline({
|
|
67
|
+
providers,
|
|
68
|
+
task: 'abc',
|
|
69
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
70
|
+
integrate: { use: 'p-int' },
|
|
71
|
+
stages: [{ id: 's1', use: 'p-dead', prompt: () => 'x' }],
|
|
72
|
+
callOpt,
|
|
73
|
+
})
|
|
74
|
+
let r = [
|
|
75
|
+
t.ok,
|
|
76
|
+
t.A.ok,
|
|
77
|
+
t.A.result !== null,
|
|
78
|
+
t.B.ok,
|
|
79
|
+
t.B.failedStage,
|
|
80
|
+
t.error.indexOf('B failed:') === 0,
|
|
81
|
+
]
|
|
82
|
+
let rr = [false, true, true, false, 's1', true]
|
|
83
|
+
assert.strict.deepEqual(r, rr)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import runRolePipeline from '../src/wkf/runRolePipeline.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('runRolePipeline', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
let providers = null
|
|
10
|
+
let callOpt = { promptPrefix: '' }
|
|
11
|
+
|
|
12
|
+
before(function() {
|
|
13
|
+
fake = createFakeCli('fake-wkf-role')
|
|
14
|
+
providers = {
|
|
15
|
+
'p-a': { kind: 'claude', exe: fake.exe, model: 'sonnet' },
|
|
16
|
+
'p-b': { kind: 'codex', exe: fake.exe, model: 'gpt-5.6-luna' },
|
|
17
|
+
'p-dead': { kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] },
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
after(function() {
|
|
22
|
+
if (fake) {
|
|
23
|
+
fake.clean()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('stages無效時回傳錯誤結果物件', async function() {
|
|
28
|
+
let t = await runRolePipeline({ providers, stages: null, callOpt })
|
|
29
|
+
let r = [t.ok, t.error, t.order]
|
|
30
|
+
let rr = [false, 'stages must be a non-empty array', []]
|
|
31
|
+
assert.strict.deepEqual(r, rr)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('各階段依序執行, ctx傳遞input與prev與results', async function() {
|
|
35
|
+
let ctxSeen = []
|
|
36
|
+
let t = await runRolePipeline({
|
|
37
|
+
providers,
|
|
38
|
+
input: 'INPUT-MARK',
|
|
39
|
+
stages: [
|
|
40
|
+
{
|
|
41
|
+
id: 'draft',
|
|
42
|
+
use: 'p-a',
|
|
43
|
+
prompt: (ctx) => {
|
|
44
|
+
ctxSeen.push(['draft', ctx.input, ctx.prev, ctx.index])
|
|
45
|
+
return `draft on ${ctx.input}`
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'review',
|
|
50
|
+
use: 'p-b',
|
|
51
|
+
prompt: (ctx) => {
|
|
52
|
+
ctxSeen.push(['review', ctx.input, ctx.prev === ctx.results.draft, ctx.index])
|
|
53
|
+
return `review ${ctx.prev.stdin}` //prev為draft階段之echo物件
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
callOpt,
|
|
58
|
+
})
|
|
59
|
+
let r = [
|
|
60
|
+
t.ok,
|
|
61
|
+
t.order,
|
|
62
|
+
t.failedStage,
|
|
63
|
+
ctxSeen,
|
|
64
|
+
t.results.draft.stdin,
|
|
65
|
+
t.results.review.stdin,
|
|
66
|
+
t.result === t.results.review, //最末階段成果即工作流成果
|
|
67
|
+
]
|
|
68
|
+
let rr = [
|
|
69
|
+
true,
|
|
70
|
+
['draft', 'review'],
|
|
71
|
+
null,
|
|
72
|
+
[['draft', 'INPUT-MARK', null, 0], ['review', 'INPUT-MARK', true, 1]],
|
|
73
|
+
'draft on INPUT-MARK',
|
|
74
|
+
'review draft on INPUT-MARK',
|
|
75
|
+
true,
|
|
76
|
+
]
|
|
77
|
+
assert.strict.deepEqual(r, rr)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('某階段失敗即中止, 已完成階段成果保留且failedStage標明斷點', async function() {
|
|
81
|
+
let t = await runRolePipeline({
|
|
82
|
+
providers,
|
|
83
|
+
stages: [
|
|
84
|
+
{ id: 's1', use: 'p-a', prompt: () => 'stage1' },
|
|
85
|
+
{ id: 's2', use: 'p-dead', prompt: () => 'stage2' },
|
|
86
|
+
{ id: 's3', use: 'p-a', prompt: () => 'stage3' },
|
|
87
|
+
],
|
|
88
|
+
callOpt,
|
|
89
|
+
})
|
|
90
|
+
let r = [
|
|
91
|
+
t.ok,
|
|
92
|
+
t.failedStage,
|
|
93
|
+
t.order,
|
|
94
|
+
t.results.s1.stdin,
|
|
95
|
+
t.results.s2,
|
|
96
|
+
t.error.indexOf('stage[s2] failed:') === 0,
|
|
97
|
+
]
|
|
98
|
+
let rr = [false, 's2', ['s1', 's2'], 'stage1', undefined, true]
|
|
99
|
+
assert.strict.deepEqual(r, rr)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('prompt非函數或回傳空值時回報該階段錯誤', async function() {
|
|
103
|
+
let t1 = await runRolePipeline({ providers, stages: [{ id: 'x', use: 'p-a', prompt: 'not-fn' }], callOpt })
|
|
104
|
+
let t2 = await runRolePipeline({ providers, stages: [{ id: 'y', use: 'p-a', prompt: () => '' }], callOpt })
|
|
105
|
+
let r = [
|
|
106
|
+
[t1.ok, t1.failedStage, t1.error],
|
|
107
|
+
[t2.ok, t2.failedStage, t2.error],
|
|
108
|
+
]
|
|
109
|
+
let rr = [
|
|
110
|
+
[false, 'x', 'stage[x].prompt must be a function'],
|
|
111
|
+
[false, 'y', 'stage[y].prompt returned empty'],
|
|
112
|
+
]
|
|
113
|
+
assert.strict.deepEqual(r, rr)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('未給id之階段自動命名stageN', async function() {
|
|
117
|
+
let t = await runRolePipeline({
|
|
118
|
+
providers,
|
|
119
|
+
stages: [{ use: 'p-a', prompt: () => 'abc' }],
|
|
120
|
+
callOpt,
|
|
121
|
+
})
|
|
122
|
+
let r = [t.ok, t.order]
|
|
123
|
+
let rr = [true, ['stage1']]
|
|
124
|
+
assert.strict.deepEqual(r, rr)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('階段可自帶fallback, 主模型失敗自動遞補', async function() {
|
|
128
|
+
let t = await runRolePipeline({
|
|
129
|
+
providers,
|
|
130
|
+
stages: [{ id: 's1', use: 'p-dead', fallback: ['p-b'], prompt: () => 'abc' }],
|
|
131
|
+
callOpt,
|
|
132
|
+
})
|
|
133
|
+
let r = [t.ok, t.stages.s1.providerId]
|
|
134
|
+
let rr = [true, 'p-b']
|
|
135
|
+
assert.strict.deepEqual(r, rr)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('rawText階段之成果為文字並可傳遞至下一階段', async function() {
|
|
139
|
+
let t = await runRolePipeline({
|
|
140
|
+
providers,
|
|
141
|
+
stages: [
|
|
142
|
+
{ id: 's1', use: 'p-a', rawText: true, prompt: () => 'abc' },
|
|
143
|
+
{ id: 's2', use: 'p-b', prompt: (ctx) => `got ${typeof ctx.prev}` },
|
|
144
|
+
],
|
|
145
|
+
callOpt,
|
|
146
|
+
})
|
|
147
|
+
let r = [t.ok, typeof t.results.s1, t.results.s2.stdin]
|
|
148
|
+
let rr = [true, 'string', 'got string']
|
|
149
|
+
assert.strict.deepEqual(r, rr)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('階段check未過視為該階段失敗', async function() {
|
|
153
|
+
let t = await runRolePipeline({
|
|
154
|
+
providers,
|
|
155
|
+
stages: [{ id: 's1', use: 'p-a', prompt: () => 'abc', check: () => false }],
|
|
156
|
+
callOpt,
|
|
157
|
+
})
|
|
158
|
+
let r = [t.ok, t.failedStage]
|
|
159
|
+
let rr = [false, 's1']
|
|
160
|
+
assert.strict.deepEqual(r, rr)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
})
|