minimal-agent 0.11.0 → 0.13.0
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/package.json +5 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +50 -20
- package/plugins/workflow-runner/src/index.ts +75 -31
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +137 -14
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +36 -4
- package/src/cli/print.ts +42 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +23 -8
- package/src/plugins/pluginRunner.ts +32 -13
- package/src/prompts/policyRegistry.js +55 -1
- package/src/prompts/policyRegistry.ts +55 -1
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +80 -15
- package/src/tools/bash/bash.ts +83 -15
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +134 -27
- package/src/tools/powershell/powershell.ts +171 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +249 -29
- package/src/tools/read/read.ts +396 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +57 -0
- package/src/tools/shared/permanentDeleteGuard.ts +71 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +22 -4
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- package/workflows/youtube-shorts.yaml +25 -25
|
@@ -144,4 +144,24 @@ describe('CLI 端到端 /workflow(拉子进程跑 main.tsx)', () => {
|
|
|
144
144
|
},
|
|
145
145
|
SPAWN_TIMEOUT,
|
|
146
146
|
);
|
|
147
|
+
|
|
148
|
+
// B7:用户 shell 习惯写 --input brief=A B C(没加引号),shell 拆成 --input brief=A + B + C
|
|
149
|
+
// 三个 token;引擎识别为首参已由 --input 提供、但仍存在散落位置参数,按贪婪语义自动拼回,
|
|
150
|
+
// 不丢 B、C(之前 B/C 被当占位参数映射给后续非贪婪 input,或直接静默丢弃)。
|
|
151
|
+
it(
|
|
152
|
+
'B7:贪婪首参用 --input 提供但没加引号被拆 → 位置参数自动拼回首参',
|
|
153
|
+
async () => {
|
|
154
|
+
// 注意:命令字符串本身在这里是单条 input,但引擎 tokenize 后是:
|
|
155
|
+
// [/workflow, greedy-e2e, --input, brief=做, 一个, 小玩具]
|
|
156
|
+
// 即 greedy-e2e 之后的内容中,--input brief=做 是首参赋值,「一个」「小玩具」
|
|
157
|
+
// 是被 shell-like 拆分出来的位置参数,B7 修复后必须自动拼回为「做 一个 小玩具」。
|
|
158
|
+
const r = await spawnMain('/workflow greedy-e2e --input brief=做 一个 小玩具', tmp);
|
|
159
|
+
expect(r.exitCode).toBe(0);
|
|
160
|
+
expect(r.stdout).toContain('GREEDY-BRIEF=[做 一个 小玩具]');
|
|
161
|
+
// 修复附带的用户提示:告诉用户下次记得加引号(-p 模式 warning 走 stderr 或 stdout 都算)
|
|
162
|
+
const combined = r.stdout + '\n' + r.stderr;
|
|
163
|
+
expect(combined).toContain('已自动拼回');
|
|
164
|
+
},
|
|
165
|
+
SPAWN_TIMEOUT,
|
|
166
|
+
);
|
|
147
167
|
});
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
* ============================================================
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
afterAll,
|
|
20
|
+
beforeAll,
|
|
21
|
+
beforeEach,
|
|
22
|
+
describe,
|
|
23
|
+
expect,
|
|
24
|
+
it,
|
|
25
|
+
mock,
|
|
26
|
+
} from 'bun:test';
|
|
27
|
+
|
|
28
|
+
import type { Provider, Tool } from '../../../src/types.js';
|
|
20
29
|
import { execLlmStep } from '../src/stepExecutors/llm.js';
|
|
21
30
|
import { VarStack } from '../src/types.js';
|
|
22
|
-
import type { Provider } from '../../../src/types.js';
|
|
23
31
|
import type { RunContext, StepDef } from '../src/types.js';
|
|
24
32
|
|
|
25
33
|
const fakeProvider: Provider = {
|
|
@@ -67,8 +75,11 @@ interface ExecuteToolCall {
|
|
|
67
75
|
}
|
|
68
76
|
const capturedExecuteToolCalls: ExecuteToolCall[] = [];
|
|
69
77
|
/** mock executeTool 返回值脚本(按调用顺序);用尽则回退到 { ok:true, content:'OK' } */
|
|
70
|
-
let mockToolResults: Array<
|
|
78
|
+
let mockToolResults: Array<
|
|
79
|
+
{ ok: true; content: string } | { ok: false; error: string }
|
|
80
|
+
> = [];
|
|
71
81
|
|
|
82
|
+
import { chat as realChatImport } from '../../../src/llm/client.js';
|
|
72
83
|
// 复用真实的 ALL_TOOLS / getToolByName(llm.ts 的白名单过滤要用),
|
|
73
84
|
// 只把 executeTool 替换成 mock 版本。
|
|
74
85
|
//
|
|
@@ -80,7 +91,6 @@ import {
|
|
|
80
91
|
executeTool as realExecuteTool,
|
|
81
92
|
getToolByName as realGetToolByName,
|
|
82
93
|
} from '../../../src/tools/index.js';
|
|
83
|
-
import { chat as realChatImport } from '../../../src/llm/client.js';
|
|
84
94
|
|
|
85
95
|
// 在 mock 安装前把真实 chat 函数引用拷到值变量,避免 ESM live binding
|
|
86
96
|
// 在 mock.module 后跟着变成 mock 版本(afterAll 复原才能拿到真函数)
|
|
@@ -140,7 +150,10 @@ function installChatMock(): void {
|
|
|
140
150
|
argumentsDelta: turn.argsJson,
|
|
141
151
|
};
|
|
142
152
|
}
|
|
143
|
-
yield {
|
|
153
|
+
yield {
|
|
154
|
+
type: 'done',
|
|
155
|
+
stopReason: turn.kind === 'tool_call' ? 'tool_calls' : 'stop',
|
|
156
|
+
};
|
|
144
157
|
},
|
|
145
158
|
}));
|
|
146
159
|
}
|
|
@@ -156,7 +169,12 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
156
169
|
mockTurns = [];
|
|
157
170
|
mockTurnIdx = 0;
|
|
158
171
|
mockToolResults = [];
|
|
159
|
-
ctx = {
|
|
172
|
+
ctx = {
|
|
173
|
+
provider: fakeProvider,
|
|
174
|
+
history: [],
|
|
175
|
+
cwd: process.cwd(),
|
|
176
|
+
tools: [],
|
|
177
|
+
};
|
|
160
178
|
});
|
|
161
179
|
|
|
162
180
|
it('pure llm 不下发工具,避免单轮设计类步骤误调工具后撞 maxTurns=1', async () => {
|
|
@@ -175,6 +193,7 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
175
193
|
});
|
|
176
194
|
|
|
177
195
|
it('case 1: 基本 Read 引用 — tool_call 一轮 + 纯文本一轮', async () => {
|
|
196
|
+
ctx.tools = [{ name: 'Read' } as Tool];
|
|
178
197
|
mockTurns = [
|
|
179
198
|
{
|
|
180
199
|
kind: 'tool_call',
|
|
@@ -190,8 +209,7 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
190
209
|
id: 'read-and-answer',
|
|
191
210
|
llm: 'please summarize',
|
|
192
211
|
context_files: [{ path: '/tmp/x.txt', hint: 'source data' }],
|
|
193
|
-
//
|
|
194
|
-
// 白名单把下发工具收敛到 Read(验证 loop.ts 的 allowedTools 过滤 + llm.ts 透传)。
|
|
212
|
+
// context_files 经 runQuery(TAOR) 执行;步骤白名单与宿主有效工具集取交集。
|
|
195
213
|
allowed_tools: ['Read'],
|
|
196
214
|
};
|
|
197
215
|
|
|
@@ -212,8 +230,9 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
212
230
|
expect(parsedArgs.file_path).toBe('/tmp/x.txt');
|
|
213
231
|
|
|
214
232
|
// 第一轮 user message 包含路径和 hint
|
|
215
|
-
const firstUserMsg = (
|
|
216
|
-
.
|
|
233
|
+
const firstUserMsg = (
|
|
234
|
+
capturedChatArgs[0].messages as Array<{ role: string; content: string }>
|
|
235
|
+
).find((m) => m.role === 'user');
|
|
217
236
|
expect(firstUserMsg).toBeDefined();
|
|
218
237
|
expect(firstUserMsg!.content).toContain('/tmp/x.txt');
|
|
219
238
|
expect(firstUserMsg!.content).toContain('source data');
|
|
@@ -225,12 +244,23 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
225
244
|
});
|
|
226
245
|
|
|
227
246
|
it('case 2: 永不收敛(每轮都调工具)→ 被 maxTurns 严格兜住,不无限循环', async () => {
|
|
247
|
+
ctx.tools = [{ name: 'Read' } as Tool];
|
|
228
248
|
// v0.9 重构移除了 miniReAct 的"重复工具调用早停",改由主 TAOR 引擎(runQuery)的
|
|
229
249
|
// maxTurns 硬上限兜底。本用例验证安全属性:模型每轮都调工具、永不收敛时,chat 调用
|
|
230
250
|
// 次数被 maxTurns 严格限制(不会无限循环),不再依赖已移除的重复检测。
|
|
231
251
|
mockTurns = [
|
|
232
|
-
{
|
|
233
|
-
|
|
252
|
+
{
|
|
253
|
+
kind: 'tool_call',
|
|
254
|
+
callId: 'c1',
|
|
255
|
+
name: 'Read',
|
|
256
|
+
argsJson: '{"file_path":"/a"}',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
kind: 'tool_call',
|
|
260
|
+
callId: 'c2',
|
|
261
|
+
name: 'Read',
|
|
262
|
+
argsJson: '{"file_path":"/a"}',
|
|
263
|
+
},
|
|
234
264
|
];
|
|
235
265
|
mockToolResults = [
|
|
236
266
|
{ ok: true, content: 'A' },
|
|
@@ -246,13 +276,14 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
246
276
|
|
|
247
277
|
// 不无限循环:chat 调用次数被 maxTurns(=2) 严格兜住。
|
|
248
278
|
// 撞顶后的半截产出不可信,execLlmStep 必须抛错触发 runner retry/halt。
|
|
249
|
-
await expect(
|
|
250
|
-
|
|
251
|
-
);
|
|
279
|
+
await expect(
|
|
280
|
+
drainLlm(execLlmStep(step, new VarStack(), ctx)),
|
|
281
|
+
).rejects.toThrow('达到最大轮数 2 被截断');
|
|
252
282
|
expect(capturedChatArgs.length).toBe(2);
|
|
253
283
|
});
|
|
254
284
|
|
|
255
285
|
it('case 3: output_schema + tools — 走 [json+tools] 前缀', async () => {
|
|
286
|
+
ctx.tools = [{ name: 'Read' } as Tool];
|
|
256
287
|
mockTurns = [
|
|
257
288
|
{
|
|
258
289
|
kind: 'tool_call',
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端到端验证 Phase 3:session 化状态目录布局(<wfDir>/<sessionId>/)
|
|
3
|
+
* 在真实 runWorkflow 下的完整行为:fresh → 同 goal completed →
|
|
4
|
+
* 不同 goal new-task + 归档 → 旧布局兼容 → 多 session 并行隔离。
|
|
5
|
+
*
|
|
6
|
+
* 复用 e2e.test.ts 的 mock 模式(mock workingDir.ts + MINIMAL_AGENT_CWD),
|
|
7
|
+
* 用 hello-workflow fixture(无 LLM 依赖,确定性强)。
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
it,
|
|
15
|
+
mock,
|
|
16
|
+
} from 'bun:test';
|
|
17
|
+
import {
|
|
18
|
+
copyFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
mkdtempSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from 'node:fs';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
|
|
30
|
+
import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.js';
|
|
31
|
+
import { _resetPluginCache } from '../../../src/plugins/commandRouter.js';
|
|
32
|
+
import type { Provider } from '../../../src/types.js';
|
|
33
|
+
import type { PluginEvent } from '../../../src/plugins/pluginApi.js';
|
|
34
|
+
import type { WorkflowEvent } from '../src/types.js';
|
|
35
|
+
|
|
36
|
+
type Ev = PluginEvent | WorkflowEvent;
|
|
37
|
+
|
|
38
|
+
const FIXTURE_SRC = join(import.meta.dir, 'fixtures', 'hello-workflow.yaml');
|
|
39
|
+
|
|
40
|
+
const provider: Provider = {
|
|
41
|
+
name: 'test',
|
|
42
|
+
baseURL: '',
|
|
43
|
+
apiKey: '',
|
|
44
|
+
model: 'm',
|
|
45
|
+
contextWindow: 128000,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function collect(gen: AsyncGenerator<Ev, void, void>): Promise<Ev[]> {
|
|
49
|
+
const out: Ev[] = [];
|
|
50
|
+
for await (const ev of gen) out.push(ev);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const SESSION_ALPHA = 'alpha';
|
|
55
|
+
const SESSION_BETA = 'beta';
|
|
56
|
+
|
|
57
|
+
describe('workflow E2E · session 化新布局(Phase 3)', () => {
|
|
58
|
+
let tmp: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
tmp = mkdtempSync(join(tmpdir(), 'wf-e2e-phase3-'));
|
|
62
|
+
mkdirSync(join(tmp, 'workflows'), { recursive: true });
|
|
63
|
+
// 拷贝 fixture 并:① 加 goal input(hash 身份的关键字段,区分任务)
|
|
64
|
+
// ② 补静态路径 artifacts 声明 + 一个写它的 step(new-task 归档逻辑依赖声明产物;
|
|
65
|
+
// 注意 artifacts 不支持 ${var} 插值,必须用固定路径)
|
|
66
|
+
const raw = readFileSync(FIXTURE_SRC, 'utf8');
|
|
67
|
+
const patched = raw
|
|
68
|
+
.replace(
|
|
69
|
+
'version: "0.1"',
|
|
70
|
+
'version: "0.1"\n\nartifacts:\n - "greetings/output.txt"',
|
|
71
|
+
)
|
|
72
|
+
.replace(
|
|
73
|
+
'inputs:\n - name: name',
|
|
74
|
+
'inputs:\n - name: goal\n type: string\n required: true\n description: 任务身份\n - name: name',
|
|
75
|
+
)
|
|
76
|
+
// 在 verify_files 前插一个写 output.txt 的 step
|
|
77
|
+
.replace(
|
|
78
|
+
' - id: verify_files',
|
|
79
|
+
` - id: write_output\n tool: Write\n args:\n file_path: "greetings/output.txt"\n content: "ok"\n\n - id: verify_files`,
|
|
80
|
+
);
|
|
81
|
+
writeFileSync(join(tmp, 'workflows', 'hello-workflow.yaml'), patched);
|
|
82
|
+
|
|
83
|
+
process.env.MINIMAL_AGENT_CWD = tmp;
|
|
84
|
+
_resetWorkingDir();
|
|
85
|
+
_resetPluginCache();
|
|
86
|
+
mock.module('../../../src/bootstrap/workingDir.js', () => ({
|
|
87
|
+
getWorkingDir: () => tmp,
|
|
88
|
+
initWorkingDir: () => tmp,
|
|
89
|
+
_resetWorkingDir: () => {},
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
delete process.env.MINIMAL_AGENT_CWD;
|
|
95
|
+
_resetWorkingDir();
|
|
96
|
+
_resetPluginCache();
|
|
97
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
async function runCmd(
|
|
101
|
+
input: string,
|
|
102
|
+
opts: { sessionId?: string } = {},
|
|
103
|
+
): Promise<Ev[]> {
|
|
104
|
+
const { runWithPlugins } = await import('../../../src/plugins/pluginRunner.js');
|
|
105
|
+
const runtimeContext: Record<string, unknown> = {};
|
|
106
|
+
if (opts.sessionId) runtimeContext.sessionId = opts.sessionId;
|
|
107
|
+
return collect(runWithPlugins(input, {
|
|
108
|
+
provider,
|
|
109
|
+
history: [],
|
|
110
|
+
runtimeContext: runtimeContext as any,
|
|
111
|
+
}) as AsyncGenerator<Ev>);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function wfDir(): string {
|
|
115
|
+
return join(tmp, '.minimal-agent-workflow', 'hello-workflow');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sessionActive(session: string): boolean {
|
|
119
|
+
return existsSync(join(wfDir(), session));
|
|
120
|
+
}
|
|
121
|
+
function sessionDone(session: string): boolean {
|
|
122
|
+
return existsSync(join(wfDir(), `${session}.done`));
|
|
123
|
+
}
|
|
124
|
+
function sessionTrash(session: string): boolean {
|
|
125
|
+
return existsSync(join(wfDir(), `${session}.trash`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it('fresh → 真实 runWorkflow 跑完归档到 <wfDir>/<sessionId>.done/(成功路径)', async () => {
|
|
129
|
+
await runCmd(
|
|
130
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
131
|
+
{ sessionId: SESSION_BETA },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// 跑完后状态被 archive,活动实例目录被改名 <sessionId>.done/
|
|
135
|
+
expect(sessionActive(SESSION_BETA)).toBe(false);
|
|
136
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
137
|
+
// 产物实际落盘到 cwd(不是状态目录里)
|
|
138
|
+
expect(existsSync(join(tmp, 'greetings/alice/hello.txt'))).toBe(true);
|
|
139
|
+
// 状态目录保留了 current.json / inputs.json / progress.md 等(供后续回溯)
|
|
140
|
+
const doneDir = join(wfDir(), `${SESSION_BETA}.done`);
|
|
141
|
+
expect(existsSync(join(doneDir, 'current.json'))).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('未指定 sessionId → 引擎自动生成唯一 session(非 default),多 session 互不覆盖', async () => {
|
|
145
|
+
// 不传 sessionId:宿主未提供有意义 session(runtimeContext.sessionId 缺失),
|
|
146
|
+
// 引擎应自动命名一个唯一 session(YYYYMMDD-HHmmss-xxxxxx),而非固定 'default'
|
|
147
|
+
await runCmd(
|
|
148
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
149
|
+
);
|
|
150
|
+
let doneEntries = readdirSync(wfDir()).filter((n) => n.endsWith('.done'));
|
|
151
|
+
expect(doneEntries.length).toBe(1);
|
|
152
|
+
const autoSession = doneEntries[0].slice(0, -'.done'.length);
|
|
153
|
+
expect(autoSession).not.toBe('default');
|
|
154
|
+
expect(/^\d{8}-\d{6}-[0-9a-f]{6}$/.test(autoSession)).toBe(true);
|
|
155
|
+
// 产物落盘
|
|
156
|
+
expect(existsSync(join(tmp, 'greetings/alice/hello.txt'))).toBe(true);
|
|
157
|
+
|
|
158
|
+
// 再跑一次(不同 goal):引擎自动生成第二个独立 session,各自归档互不覆盖
|
|
159
|
+
await runCmd(
|
|
160
|
+
'/workflow hello-workflow --input goal=任务X --input name=carol --input lang=en',
|
|
161
|
+
);
|
|
162
|
+
doneEntries = readdirSync(wfDir()).filter((n) => n.endsWith('.done'));
|
|
163
|
+
expect(doneEntries.length).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('fresh + 同 goal 再跑 → completed(命中 .done/ 归档,第二次不写产物)', async () => {
|
|
167
|
+
await runCmd(
|
|
168
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
169
|
+
{ sessionId: SESSION_BETA },
|
|
170
|
+
);
|
|
171
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
172
|
+
|
|
173
|
+
const events = await runCmd(
|
|
174
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
175
|
+
{ sessionId: SESSION_BETA },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// 第二次是 completed:.done 归档不变(同一任务已完成,不重新归档)
|
|
179
|
+
expect(sessionActive(SESSION_BETA)).toBe(false);
|
|
180
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
181
|
+
// 产物保留(completed 不删)
|
|
182
|
+
expect(existsSync(join(tmp, 'greetings/alice/hello.txt'))).toBe(true);
|
|
183
|
+
// 事件序列表明已完成分支被命中
|
|
184
|
+
expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('goal A 完成 → 改 goal B(同 session 内 new-task):B 产物覆盖,A 产物归档到 .trash/<stamp>/', async () => {
|
|
188
|
+
await runCmd(
|
|
189
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
190
|
+
{ sessionId: SESSION_BETA },
|
|
191
|
+
);
|
|
192
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
193
|
+
expect(existsSync(join(tmp, 'greetings/output.txt'))).toBe(true);
|
|
194
|
+
|
|
195
|
+
// 改 goal → hash 不同 → new-task(同 session 内,A 被归档后重跑 B)
|
|
196
|
+
await runCmd(
|
|
197
|
+
'/workflow hello-workflow --input goal=任务B --input name=bob --input lang=zh',
|
|
198
|
+
{ sessionId: SESSION_BETA },
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// 最终仍是 .done(B 成功)
|
|
202
|
+
expect(sessionActive(SESSION_BETA)).toBe(false);
|
|
203
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
204
|
+
// B 产物存在
|
|
205
|
+
expect(existsSync(join(tmp, 'greetings/bob/hello.txt'))).toBe(true);
|
|
206
|
+
// 声明产物(output.txt)被 goal B 重新生成(write_output step 又跑了一遍)
|
|
207
|
+
expect(existsSync(join(tmp, 'greetings/output.txt'))).toBe(true);
|
|
208
|
+
// A 的旧产物被归档到 .trash 里(goal A 的旧 output.txt)
|
|
209
|
+
const trashDir = join(wfDir(), `${SESSION_BETA}.trash`);
|
|
210
|
+
expect(sessionTrash(SESSION_BETA)).toBe(true);
|
|
211
|
+
const outputInTrash = flattenDir(trashDir).some((f) =>
|
|
212
|
+
f.endsWith('output.txt'),
|
|
213
|
+
);
|
|
214
|
+
expect(outputInTrash).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('同 goal 两个 session → 各自独立完成(Phase 3 核心:多 session 并行隔离)', async () => {
|
|
218
|
+
// session alpha 跑 goal=任务A
|
|
219
|
+
await runCmd(
|
|
220
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
221
|
+
{ sessionId: SESSION_ALPHA },
|
|
222
|
+
);
|
|
223
|
+
// session beta 跑 goal=任务A(同 goal,不同 session)
|
|
224
|
+
await runCmd(
|
|
225
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
226
|
+
{ sessionId: SESSION_BETA },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// 两个 session 都有自己的 .done 归档,互不干扰
|
|
230
|
+
expect(sessionDone(SESSION_ALPHA)).toBe(true);
|
|
231
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
232
|
+
// 各自的 current.json 都在
|
|
233
|
+
expect(existsSync(join(wfDir(), `${SESSION_ALPHA}.done`, 'current.json'))).toBe(true);
|
|
234
|
+
expect(existsSync(join(wfDir(), `${SESSION_BETA}.done`, 'current.json'))).toBe(true);
|
|
235
|
+
// 产物(共享 cwd 下)都存在
|
|
236
|
+
expect(existsSync(join(tmp, 'greetings/alice/hello.txt'))).toBe(true);
|
|
237
|
+
expect(existsSync(join(tmp, 'greetings/output.txt'))).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('不同 session 的 new-task 互不影响:A session 跑 goalA,B session 跑 goalB 时 A 完好', async () => {
|
|
241
|
+
// session alpha: goalA 完成
|
|
242
|
+
await runCmd(
|
|
243
|
+
'/workflow hello-workflow --input goal=任务A --input name=alice --input lang=en',
|
|
244
|
+
{ sessionId: SESSION_ALPHA },
|
|
245
|
+
);
|
|
246
|
+
expect(sessionDone(SESSION_ALPHA)).toBe(true);
|
|
247
|
+
const alphaBefore = readFileSync(
|
|
248
|
+
join(wfDir(), `${SESSION_ALPHA}.done`, 'current.json'),
|
|
249
|
+
'utf8',
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// session beta: 跑 goalB(new-task,不应碰 alpha 的任何东西)
|
|
253
|
+
await runCmd(
|
|
254
|
+
'/workflow hello-workflow --input goal=任务B --input name=bob --input lang=zh',
|
|
255
|
+
{ sessionId: SESSION_BETA },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// alpha 的 .done 完好无损
|
|
259
|
+
expect(sessionDone(SESSION_ALPHA)).toBe(true);
|
|
260
|
+
const alphaAfter = readFileSync(
|
|
261
|
+
join(wfDir(), `${SESSION_ALPHA}.done`, 'current.json'),
|
|
262
|
+
'utf8',
|
|
263
|
+
);
|
|
264
|
+
expect(alphaAfter).toBe(alphaBefore);
|
|
265
|
+
// alpha 的产物归档(.trash)也没被 beta session 碰
|
|
266
|
+
// beta 自己的 .done 也有了
|
|
267
|
+
expect(sessionDone(SESSION_BETA)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('旧布局状态文件(wfDir/current.json)→ 新引擎正确识别 + 处理(兼容)', async () => {
|
|
271
|
+
// 手写旧布局状态(status=running,未完成)
|
|
272
|
+
mkdirSync(wfDir(), { recursive: true });
|
|
273
|
+
writeFileSync(
|
|
274
|
+
join(wfDir(), 'current.json'),
|
|
275
|
+
JSON.stringify({
|
|
276
|
+
name: 'hello-workflow',
|
|
277
|
+
taskId: '1234567890abcdef',
|
|
278
|
+
status: 'running',
|
|
279
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
280
|
+
artifacts: [],
|
|
281
|
+
defFingerprint: 'legacy',
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// 跑真实工作流:新引擎应当从旧布局读到状态,根据 hash 走对应分支
|
|
286
|
+
const events = await runCmd(
|
|
287
|
+
'/workflow hello-workflow --input goal=任务C --input name=carol --input lang=en',
|
|
288
|
+
{ sessionId: SESSION_BETA },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// 核心断言:新引擎成功处理(无 error),最终有归档或活动实例
|
|
292
|
+
expect(events.filter((e) => e.type === 'error').length).toBe(0);
|
|
293
|
+
expect(sessionDone(SESSION_BETA) || sessionActive(SESSION_BETA)).toBe(true);
|
|
294
|
+
// carol 产物落盘
|
|
295
|
+
expect(existsSync(join(tmp, 'greetings/carol/hello.txt'))).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/** 递归列出目录下所有文件路径 */
|
|
300
|
+
function flattenDir(dir: string): string[] {
|
|
301
|
+
const out: string[] = [];
|
|
302
|
+
const walk = (d: string) => {
|
|
303
|
+
for (const e of readdirSync(d)) {
|
|
304
|
+
const p = join(d, e);
|
|
305
|
+
const isDir = existsSync(p) && (() => {
|
|
306
|
+
try { readdirSync(p); return true; } catch { return false; }
|
|
307
|
+
})();
|
|
308
|
+
if (isDir) walk(p);
|
|
309
|
+
else out.push(p);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
walk(dir);
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
@@ -20,6 +20,7 @@ import { tmpdir } from 'node:os';
|
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
|
|
22
22
|
import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.js';
|
|
23
|
+
import { createAgentRuntimeContext } from '../../../src/plugin-sdk.js';
|
|
23
24
|
import type { LoopEvent, Provider } from '../../../src/types.js';
|
|
24
25
|
import { evalExpr, interpolate } from '../src/expressions.js';
|
|
25
26
|
import { runWorkflow } from '../src/runner.js';
|
|
@@ -80,7 +81,14 @@ describe('E3/E2/E1 引擎层(runWorkflow 集成)', () => {
|
|
|
80
81
|
initWorkingDir: () => tmp,
|
|
81
82
|
_resetWorkingDir: () => {},
|
|
82
83
|
}));
|
|
83
|
-
ctx = {
|
|
84
|
+
ctx = {
|
|
85
|
+
provider: fakeProvider,
|
|
86
|
+
history: [],
|
|
87
|
+
cwd: tmp,
|
|
88
|
+
tools: [],
|
|
89
|
+
// 显式 sessionId:resume(两次 runWorkflow 同 session)场景可预测
|
|
90
|
+
runtimeContext: createAgentRuntimeContext({ surface: 'workflow', profile: 'main', cwd: tmp, sessionId: 'test-session', owner: 'local-owner', interactiveAsk: false }),
|
|
91
|
+
};
|
|
84
92
|
});
|
|
85
93
|
|
|
86
94
|
afterEach(() => {
|
|
@@ -270,6 +270,48 @@ describe('workflows/expressions::fileExists', () => {
|
|
|
270
270
|
expect(evalExpr('fileExists(inputs.name + ".txt")', vars)).toBe(true);
|
|
271
271
|
expect(evalExpr('fileExists("videos/" + inputs.name + ".txt")', vars)).toBe(false);
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
// 会话进入 session worktree 后,写文件的工具跟着 worktree 走(loop.ts 按
|
|
275
|
+
// sessionState.activeWorktree 重定向 cwd)。done_when / when / over 全靠
|
|
276
|
+
// fileExists,若它仍锚原 workspace,就会在空目录里找产物 —— 恒 false,
|
|
277
|
+
// 工作流被判失败并反复重试,而产物其实好好躺在 worktree 里。
|
|
278
|
+
it('resolveCwd 让 fileExists 跟随 session worktree 而非原 workspace', () => {
|
|
279
|
+
const worktree = mkdtempSync(join(tmpdir(), 'wf-expr-wt-'));
|
|
280
|
+
try {
|
|
281
|
+
// 产物只存在于 worktree,workspace 是空的。
|
|
282
|
+
writeFileSync(join(worktree, 'out.md'), 'artifact');
|
|
283
|
+
|
|
284
|
+
const vars = new VarStack();
|
|
285
|
+
// 未设 resolveCwd:锚 workspace(旧行为)→ 找不到。
|
|
286
|
+
expect(evalExpr('fileExists("out.md")', vars)).toBe(false);
|
|
287
|
+
|
|
288
|
+
// 设了 resolveCwd:锚 worktree → 命中。
|
|
289
|
+
vars.resolveCwd = () => worktree;
|
|
290
|
+
expect(evalExpr('fileExists("out.md")', vars)).toBe(true);
|
|
291
|
+
|
|
292
|
+
// 解析器是函数而非快照:运行途中退出 worktree 后立刻跟着变回去。
|
|
293
|
+
let active: string | undefined = worktree;
|
|
294
|
+
vars.resolveCwd = () => active ?? tmp;
|
|
295
|
+
expect(evalExpr('fileExists("out.md")', vars)).toBe(true);
|
|
296
|
+
active = undefined;
|
|
297
|
+
expect(evalExpr('fileExists("out.md")', vars)).toBe(false);
|
|
298
|
+
} finally {
|
|
299
|
+
rmSync(worktree, { recursive: true, force: true });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('resolveCwd 下越界守卫仍以该 cwd 为界', () => {
|
|
304
|
+
const worktree = mkdtempSync(join(tmpdir(), 'wf-expr-wt2-'));
|
|
305
|
+
try {
|
|
306
|
+
writeFileSync(join(tmp, 'outside.txt'), 'x');
|
|
307
|
+
const vars = new VarStack();
|
|
308
|
+
vars.resolveCwd = () => worktree;
|
|
309
|
+
// 换锚点后,原 workspace 的文件反而成了「界外」,fail-closed 仍生效。
|
|
310
|
+
expect(evalExpr('fileExists("../outside.txt")', vars)).toBe(false);
|
|
311
|
+
} finally {
|
|
312
|
+
rmSync(worktree, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
273
315
|
});
|
|
274
316
|
|
|
275
317
|
describe('workflows/expressions::readJson', () => {
|
|
@@ -25,6 +25,7 @@ describe('框架侧:runCommand 必须带上 host', () => {
|
|
|
25
25
|
expect(call).toContain('host:');
|
|
26
26
|
expect(call).toContain('computerRun');
|
|
27
27
|
expect(call).toContain('browserRun');
|
|
28
|
+
expect(call).toContain('executeTool: options.executeTool');
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
test('PluginContext 用 Pick<ToolCallContext> 绑定,而不是重新声明', async () => {
|
|
@@ -40,9 +41,14 @@ describe('能力常量单一来源', () => {
|
|
|
40
41
|
test('loader 的白名单与 types 的 HostCapability 一致', async () => {
|
|
41
42
|
// 两处不一致的表现是:yaml 写了某个能力、loader 放行、运行时却识别不了,
|
|
42
43
|
// 或反过来 loader 拦掉一个其实支持的能力。都属于静默错。
|
|
43
|
-
const types = await Bun.file(
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const types = await Bun.file(
|
|
45
|
+
new URL('../src/types.ts', import.meta.url),
|
|
46
|
+
).text();
|
|
47
|
+
const declared =
|
|
48
|
+
/export type HostCapability =([^;]+);/.exec(types)?.[1] ?? '';
|
|
49
|
+
const fromTypes = [...declared.matchAll(/'([a-z]+)'/g)]
|
|
50
|
+
.map((m) => m[1])
|
|
51
|
+
.sort();
|
|
46
52
|
expect(fromTypes).toEqual([...VALID_HOST_CAPABILITIES].sort());
|
|
47
53
|
});
|
|
48
54
|
});
|
|
@@ -52,7 +52,7 @@ describe('workflows/illustrate.yaml(网上抓图配图)', () => {
|
|
|
52
52
|
expect(inputs.find((i) => i.name === 'topic')?.greedy).toBe(true);
|
|
53
53
|
expect(inputs.find((i) => i.name === 'count')?.default).toBe(3);
|
|
54
54
|
expect(inputs.find((i) => i.name === 'min_candidates')?.default).toBe(6);
|
|
55
|
-
expect(def.artifacts).toContain('
|
|
55
|
+
expect(def.artifacts).toContain('out/illustrate/${session}/build/illustrations.json');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
it('关键步齐全且顺序正确:三家图搜 → 守门 → 下载 → 打分 → 选优 → 报告', async () => {
|
|
@@ -152,7 +152,7 @@ describe('workflows/illustrate.yaml(网上抓图配图)', () => {
|
|
|
152
152
|
expect(a).toContain('illustrations.json');
|
|
153
153
|
expect(String(sel?.done_when)).toContain('illustrations.json');
|
|
154
154
|
for (const t of ['Glob', 'Read', 'Write']) expect(sel?.allowed_tools).toContain(t);
|
|
155
|
-
expect(sel?.artifacts).toContain('
|
|
155
|
+
expect(sel?.artifacts).toContain('out/illustrate/${session}/build/illustrations.json');
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
it('用到本轮新增通用件:parseJson 表达式 + Download 工具', async () => {
|