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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, expect, it } from 'bun:test';
|
|
7
7
|
|
|
8
|
+
import { ALL_TOOLS } from '../../../src/tools/index.js';
|
|
8
9
|
import type {
|
|
9
10
|
ChatContentPart,
|
|
10
11
|
LlmStreamEvent,
|
|
@@ -16,7 +17,6 @@ import { execAgentStep } from '../src/stepExecutors/agent.js';
|
|
|
16
17
|
import { execLlmStep } from '../src/stepExecutors/llm.js';
|
|
17
18
|
import { VarStack } from '../src/types.js';
|
|
18
19
|
import type { RunContext, StepDef } from '../src/types.js';
|
|
19
|
-
import { ALL_TOOLS } from '../../../src/tools/index.js';
|
|
20
20
|
|
|
21
21
|
const provider: Provider = {
|
|
22
22
|
name: 'test',
|
|
@@ -72,7 +72,11 @@ describe('workflow-runner multimodal images', () => {
|
|
|
72
72
|
const chat = async function* (): AsyncGenerator<LlmStreamEvent> {
|
|
73
73
|
if (round++ === 0) {
|
|
74
74
|
yield { type: 'tool_call_delta', index: 0, id: 'hidden', name: 'Bash' };
|
|
75
|
-
yield {
|
|
75
|
+
yield {
|
|
76
|
+
type: 'tool_call_delta',
|
|
77
|
+
index: 0,
|
|
78
|
+
argumentsDelta: '{"command":"echo no"}',
|
|
79
|
+
};
|
|
76
80
|
yield { type: 'done', stopReason: 'tool_calls' };
|
|
77
81
|
return;
|
|
78
82
|
}
|
|
@@ -90,15 +94,58 @@ describe('workflow-runner multimodal images', () => {
|
|
|
90
94
|
return { ok: true, content: 'unexpected' };
|
|
91
95
|
},
|
|
92
96
|
};
|
|
93
|
-
const result = await drain(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const result = await drain(
|
|
98
|
+
execAgentStep(
|
|
99
|
+
{ id: 'bounded', agent: '完成任务', allowed_tools: ['Bash'] },
|
|
100
|
+
new VarStack(),
|
|
101
|
+
ctx,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
98
104
|
expect(executed).toBe(0);
|
|
99
105
|
expect(result.raw.text).toBe('已改道。');
|
|
100
106
|
});
|
|
101
107
|
|
|
108
|
+
it('an empty host tool catalog remains empty instead of restoring global tools', async () => {
|
|
109
|
+
let round = 0;
|
|
110
|
+
let executed = 0;
|
|
111
|
+
const chat = async function* (): AsyncGenerator<LlmStreamEvent> {
|
|
112
|
+
if (round++ === 0) {
|
|
113
|
+
yield { type: 'tool_call_delta', index: 0, id: 'hidden', name: 'Read' };
|
|
114
|
+
yield {
|
|
115
|
+
type: 'tool_call_delta',
|
|
116
|
+
index: 0,
|
|
117
|
+
argumentsDelta: '{"file_path":"README.md"}',
|
|
118
|
+
};
|
|
119
|
+
yield { type: 'done', stopReason: 'tool_calls' };
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
yield { type: 'text_delta', delta: '没有可用工具。' };
|
|
123
|
+
yield { type: 'done', stopReason: 'stop' };
|
|
124
|
+
};
|
|
125
|
+
const ctx: RunContext = {
|
|
126
|
+
provider,
|
|
127
|
+
history: [{ role: 'system', content: 'stable' }],
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
tools: [],
|
|
130
|
+
chat,
|
|
131
|
+
executeTool: async () => {
|
|
132
|
+
executed += 1;
|
|
133
|
+
return { ok: true, content: 'unexpected' };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = await drain(
|
|
138
|
+
execAgentStep(
|
|
139
|
+
{ id: 'empty-bound', agent: '完成任务', allowed_tools: ['Read'] },
|
|
140
|
+
new VarStack(),
|
|
141
|
+
ctx,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(executed).toBe(0);
|
|
146
|
+
expect(result.raw.text).toBe('没有可用工具。');
|
|
147
|
+
});
|
|
148
|
+
|
|
102
149
|
it('execLlmStep 把 images 传成 image_url content block,并支持变量插值', async () => {
|
|
103
150
|
const captured: Message[][] = [];
|
|
104
151
|
const ctx = createContext(captured);
|
|
@@ -195,7 +242,9 @@ describe('workflow-runner multimodal images', () => {
|
|
|
195
242
|
|
|
196
243
|
expect(result.raw.text).toBe('图片可用');
|
|
197
244
|
expect(messagesContainImage(captured[0])).toBe(true);
|
|
198
|
-
expect(ctx.history).toEqual([
|
|
245
|
+
expect(ctx.history).toEqual([
|
|
246
|
+
{ role: 'system', content: 'workflow system prompt' },
|
|
247
|
+
]);
|
|
199
248
|
});
|
|
200
249
|
|
|
201
250
|
it('isolated 多模态步骤之后的普通 agent 不会收到前一步 image_url', async () => {
|
|
@@ -262,7 +311,7 @@ describe('workflow-runner multimodal images', () => {
|
|
|
262
311
|
provider,
|
|
263
312
|
history: [{ role: 'system', content: 'workflow system prompt' }],
|
|
264
313
|
cwd: process.cwd(),
|
|
265
|
-
tools:
|
|
314
|
+
tools: ALL_TOOLS.filter((tool) => tool.name === 'Write'),
|
|
266
315
|
chat,
|
|
267
316
|
executeTool: async () => {
|
|
268
317
|
toolCalled = true;
|
|
@@ -285,7 +334,9 @@ describe('workflow-runner multimodal images', () => {
|
|
|
285
334
|
|
|
286
335
|
expect(toolCalled).toBe(true);
|
|
287
336
|
expect(result.raw.text).toBe('文件已写好。');
|
|
288
|
-
expect(ctx.history).toEqual([
|
|
337
|
+
expect(ctx.history).toEqual([
|
|
338
|
+
{ role: 'system', content: 'workflow system prompt' },
|
|
339
|
+
]);
|
|
289
340
|
expect(captured.length).toBe(2);
|
|
290
341
|
});
|
|
291
342
|
});
|
|
@@ -61,6 +61,25 @@ describe('parseWorkflowArgs / tokenizeArgs 边界行为冻结', () => {
|
|
|
61
61
|
expect(r.inputs).toEqual({ topic: 'AI 教程' });
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test('引号在 = 后的 KV:value 含空格也完整捕获(回归:曾按空白硬切+引号残留)', () => {
|
|
65
|
+
// 用户 shell 习惯 `--input topic="带空格 的值"` —— 引号在 token 内部(= 之后),
|
|
66
|
+
// 曾被 tokenizeArgs 按空白切分 → 值截断 + 引号残留 + 散落 positional。
|
|
67
|
+
const r = parseWorkflowArgs(
|
|
68
|
+
'hello --input topic="比特币量化 长线 短线策略" --input depth=standard',
|
|
69
|
+
);
|
|
70
|
+
expect(r.inputs).toEqual({
|
|
71
|
+
topic: '比特币量化 长线 短线策略',
|
|
72
|
+
depth: 'standard',
|
|
73
|
+
});
|
|
74
|
+
expect(r.positional).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('单引号在 = 后同理支持', () => {
|
|
78
|
+
const r = parseWorkflowArgs("hello --input topic='AI 教程 入门'");
|
|
79
|
+
expect(r.inputs).toEqual({ topic: 'AI 教程 入门' });
|
|
80
|
+
expect(r.positional).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
64
83
|
test('未知 -- flag 跳过其后一个 token(保守解析)', () => {
|
|
65
84
|
const r = parseWorkflowArgs('hello --foo bar alice');
|
|
66
85
|
// --foo bar 被吃掉,alice 进 positional
|
|
@@ -93,11 +93,11 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
93
93
|
expect(String(s.title)).toContain('/22');
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// 收尾 echo;artifacts 只列最终交付物、不在被 rm 的
|
|
96
|
+
// 收尾 echo;artifacts 只列最终交付物、不在被 rm 的 out/ppt/${session}/build 下
|
|
97
97
|
expect(def.steps.find((s) => s.id === 'final_report')?.type).toBe('echo');
|
|
98
98
|
const declared = def.artifacts ?? [];
|
|
99
|
-
expect(declared).toContain('
|
|
100
|
-
for (const a of declared) expect(a.startsWith('
|
|
99
|
+
expect(declared).toContain('out/ppt/${session}/deck.pptx');
|
|
100
|
+
for (const a of declared) expect(a.startsWith('out/ppt/${session}/build')).toBe(false);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
it('stage0_clarify:样式推荐按配色感知、覆盖全部 13 个 VI(不再只露 6 个)', async () => {
|
|
@@ -205,7 +205,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
205
205
|
|
|
206
206
|
const repair = def.steps.find((s) => s.id === 'stage_draft_coverage_repair');
|
|
207
207
|
expect(String(repair?.when)).toContain('draft_coverage_report.json');
|
|
208
|
-
expect(String(repair?.when)).toContain("readJson('
|
|
208
|
+
expect(String(repair?.when)).toContain("readJson('out/ppt/${session}/build/draft_coverage_report.json', 'ok') == false");
|
|
209
209
|
// 用户点名最高优先级问题:fast/best 都要跑,when 不能限死 quality=='best'。
|
|
210
210
|
expect(String(repair?.when)).not.toContain("quality == 'best'");
|
|
211
211
|
expect(String(repair?.agent)).toContain('draft_coverage_report.md');
|
|
@@ -482,7 +482,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
482
482
|
// ① 摄取后跑确定性规划(在 design 之前,data 已齐)
|
|
483
483
|
const ids = def.steps.map((s) => s.id);
|
|
484
484
|
const am = def.steps.find((s) => s.id === 'stage_assemble_material');
|
|
485
|
-
expect(String(am?.args?.command)).toContain('plan_source_layout.py --build
|
|
485
|
+
expect(String(am?.args?.command)).toContain('plan_source_layout.py --build out/ppt/${session}/build');
|
|
486
486
|
expect(ids.indexOf('stage_assemble_material')).toBeLessThan(ids.indexOf('stage_design_doc'));
|
|
487
487
|
|
|
488
488
|
// ② 设计步「读规划、照它执行」——逐图心算被收成确定性
|
|
@@ -516,7 +516,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
516
516
|
expect(ga).toContain('--ensure-page-backgrounds');
|
|
517
517
|
expect(ga).toContain('image_prompt_rewrite.py');
|
|
518
518
|
expect(ga).toContain('image_plan_lint.py');
|
|
519
|
-
expect(ga).toContain('touch
|
|
519
|
+
expect(ga).toContain('touch out/ppt/${session}/build/img_route_ok');
|
|
520
520
|
// when 放宽:img_plan 存在 或 规划里有待增强图 —— 表达式可被引擎求值(不抛、不畸形)
|
|
521
521
|
const whenExpr = String(gate?.when);
|
|
522
522
|
expect(whenExpr).toContain('enhance_entries');
|
|
@@ -664,8 +664,8 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
664
664
|
expect(gen?.agent).toBeUndefined();
|
|
665
665
|
expect(gen?.tool).toBe('GenerateImage');
|
|
666
666
|
expect(gen?.tool).not.toBe('Bash');
|
|
667
|
-
expect(String(gen?.args?.plan)).toBe('
|
|
668
|
-
expect(String(gen?.args?.assets)).toBe('
|
|
667
|
+
expect(String(gen?.args?.plan)).toBe('out/ppt/${session}/build/img_plan.json');
|
|
668
|
+
expect(String(gen?.args?.assets)).toBe('out/ppt/${session}/build/assets');
|
|
669
669
|
expect(String(gen?.args?.timeout_ms)).toBe('300000');
|
|
670
670
|
expect(String(gen?.args?.wall_budget_ms)).toBe('3600000');
|
|
671
671
|
expect(gen?.onError).toBe('continue');
|
|
@@ -673,7 +673,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
673
673
|
const quality = def.steps.find((s) => s.id === 'stage_image_quality_gate');
|
|
674
674
|
expect(quality?.tool).toBe('GenerateImage');
|
|
675
675
|
expect(String(quality?.when)).toContain('image_generation_report.json');
|
|
676
|
-
expect(String(quality?.args?.retry_degraded_report)).toBe('
|
|
676
|
+
expect(String(quality?.args?.retry_degraded_report)).toBe('out/ppt/${session}/build/image_generation_report.json');
|
|
677
677
|
expect(String(quality?.title)).toContain('fast一轮');
|
|
678
678
|
expect(quality?.args?.max_rounds).toBe(1);
|
|
679
679
|
|
|
@@ -705,7 +705,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
705
705
|
const imageRetry = def.steps.find((s) => s.id === 'stage_generated_image_retry');
|
|
706
706
|
expect(imageRetry?.tool).toBe('GenerateImage');
|
|
707
707
|
expect(String(imageRetry?.when)).toBe('false');
|
|
708
|
-
expect(String(imageRetry?.args?.retry_degraded_report)).toBe('
|
|
708
|
+
expect(String(imageRetry?.args?.retry_degraded_report)).toBe('out/ppt/${session}/build/generated_image_semantic_retry_report.json');
|
|
709
709
|
expect(String(imageRetry?.args?.max_rounds)).toBe('3');
|
|
710
710
|
|
|
711
711
|
const visualGate = def.steps.find((s) => s.id === 'stage_visual_quality_gate');
|
|
@@ -753,7 +753,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
753
753
|
// 完全同参双跑、且视觉门达轮数已带残留放行)——style 检查唯一跑点在
|
|
754
754
|
// stage_visual_quality_gate(fast/best 统一 2 轮修复),导出只验哨兵。
|
|
755
755
|
expect(String(exp?.args?.command)).not.toContain('deck_visual_style_check.py');
|
|
756
|
-
expect(String(exp?.args?.command)).toContain('test -f
|
|
756
|
+
expect(String(exp?.args?.command)).toContain('test -f out/ppt/${session}/build/visual_quality_ok');
|
|
757
757
|
expect(exp?.done_when).toBeUndefined();
|
|
758
758
|
|
|
759
759
|
const design = def.steps.find((s) => s.id === 'stage_design_doc');
|
|
@@ -829,7 +829,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
829
829
|
expect(String(probe?.args?.command)).toContain('build_probe.json');
|
|
830
830
|
expect(String(probe?.args?.command)).toContain('build_complete');
|
|
831
831
|
const batchStep = dos.find((s) => s.id === 'build_batch');
|
|
832
|
-
expect(String(batchStep?.when)).toContain("!fileExists('
|
|
832
|
+
expect(String(batchStep?.when)).toContain("!fileExists('out/ppt/${session}/build/build_complete')");
|
|
833
833
|
expect(batchStep?.maxTurns).toBeLessThanOrEqual(24);
|
|
834
834
|
expect(batchStep?.max_retries).toBe(1);
|
|
835
835
|
const batch = String(batchStep?.agent);
|
|
@@ -840,7 +840,7 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
840
840
|
expect(batch).toContain('次顶层'); // 插图图层:文本之下、背景之上
|
|
841
841
|
expect(batch).toContain('遮挡');
|
|
842
842
|
const done = dos.find((s) => s.id === 'build_done');
|
|
843
|
-
expect(String(done?.when)).toContain("fileExists('
|
|
843
|
+
expect(String(done?.when)).toContain("fileExists('out/ppt/${session}/build/build_complete')");
|
|
844
844
|
});
|
|
845
845
|
|
|
846
846
|
it('fast 链路清理旧运行哨兵,且说明/报告顺序为先生图后构建', async () => {
|
|
@@ -866,12 +866,12 @@ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
|
|
|
866
866
|
]) {
|
|
867
867
|
expect(cmd).toContain(marker);
|
|
868
868
|
}
|
|
869
|
-
expect(cmd).toContain('rm -rf
|
|
869
|
+
expect(cmd).toContain('rm -rf out/ppt/${session}/build/export out/ppt/${session}/build/generated-image-reviews');
|
|
870
870
|
|
|
871
871
|
const cleanup = def.steps.find((s) => s.id === 'cleanup_intermediates');
|
|
872
872
|
const cleanupCmd = String(cleanup?.args?.command);
|
|
873
873
|
expect(cleanupCmd).toContain('inputs.debug_keep_build');
|
|
874
|
-
expect(cleanupCmd).toContain('rm -rf
|
|
874
|
+
expect(cleanupCmd).toContain('rm -rf out/ppt/${session}/build');
|
|
875
875
|
expect(cleanupCmd).toContain('debug_keep_build=true');
|
|
876
876
|
expect(cleanupCmd).not.toContain('[TEMP]');
|
|
877
877
|
expect(cleanupCmd).not.toContain('上线前');
|
|
@@ -128,11 +128,12 @@ describe('ralph-loop.yaml 端到端(while + fresh_context + 哨兵协议)',
|
|
|
128
128
|
expect(p2).not.toContain('我做了规划');
|
|
129
129
|
|
|
130
130
|
// 教训档案恰好一条、不重复(iter1 无 learning 标签时 extract → '' 不得重复累积)
|
|
131
|
-
//
|
|
131
|
+
// learnings 是 workflow 级共享存档,写在 ralph-loop/ 下(Phase 2 父目录不参与归档改名;
|
|
132
|
+
// 状态实例归档为 ralph-loop/<taskId>.done/)
|
|
132
133
|
const learningsPath = join(
|
|
133
134
|
tmp,
|
|
134
135
|
'.minimal-agent-workflow',
|
|
135
|
-
'ralph-loop
|
|
136
|
+
'ralph-loop',
|
|
136
137
|
'learnings.md',
|
|
137
138
|
);
|
|
138
139
|
expect(existsSync(learningsPath)).toBe(true);
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test/research.load.test.ts
|
|
3
|
+
* ------------------------------------------------------------
|
|
4
|
+
* 锁定 workflows/research.yaml(技术研究工作流:定义问题→测绘地图→方案对比→
|
|
5
|
+
* 深度调研→动手验证→综合升华→补洞→报告拼装)的「可解析 + 关键骨架 + 所有表达式合法」。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'bun:test';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { evalExpr, interpolate } from '../src/expressions.js';
|
|
12
|
+
import { parseWorkflowFile } from '../src/loader.js';
|
|
13
|
+
import { VarStack } from '../src/types.js';
|
|
14
|
+
import type { StepDef } from '../src/types.js';
|
|
15
|
+
|
|
16
|
+
const wfPath = join(
|
|
17
|
+
import.meta.dir.replace(/[/\\]test$/, ''),
|
|
18
|
+
'..',
|
|
19
|
+
'..',
|
|
20
|
+
'workflows',
|
|
21
|
+
'research.yaml',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
function allStrings(def: unknown): string[] {
|
|
25
|
+
const out: string[] = [];
|
|
26
|
+
const walk = (v: unknown): void => {
|
|
27
|
+
if (typeof v === 'string') out.push(v);
|
|
28
|
+
else if (Array.isArray(v)) for (const x of v) walk(x);
|
|
29
|
+
else if (v && typeof v === 'object')
|
|
30
|
+
for (const x of Object.values(v as Record<string, unknown>)) walk(x);
|
|
31
|
+
};
|
|
32
|
+
walk(def);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function allStepsRec(steps: StepDef[]): StepDef[] {
|
|
37
|
+
const out: StepDef[] = [];
|
|
38
|
+
const go = (s: StepDef) => {
|
|
39
|
+
out.push(s);
|
|
40
|
+
if (Array.isArray(s.do)) s.do.forEach(go);
|
|
41
|
+
if (Array.isArray(s.then)) s.then.forEach(go);
|
|
42
|
+
if (Array.isArray(s.else)) s.else.forEach(go);
|
|
43
|
+
};
|
|
44
|
+
steps.forEach(go);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('workflows/research.yaml(技术研究工作流)', () => {
|
|
49
|
+
it('解析通过、name=research、inputs 正确', async () => {
|
|
50
|
+
const def = await parseWorkflowFile(wfPath);
|
|
51
|
+
expect(def.name).toBe('research');
|
|
52
|
+
expect(typeof def.description).toBe('string');
|
|
53
|
+
expect(def.description.length).toBeGreaterThan(10);
|
|
54
|
+
|
|
55
|
+
const inputs = def.inputs ?? [];
|
|
56
|
+
const names = inputs.map((i) => i.name);
|
|
57
|
+
expect(names).toEqual(['topic', 'depth', 'max_iterations']);
|
|
58
|
+
expect(inputs[0].greedy).toBe(true);
|
|
59
|
+
expect(inputs[0].required).toBe(true);
|
|
60
|
+
expect(inputs[1].default).toBe('standard');
|
|
61
|
+
expect(inputs[2].default).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('骨架齐全:init -> s0(goal) -> s0_pause -> s1(map) -> [early-exit 判定] -> s2(options) -> s3(per-branch 深挖 loop) -> s4(verify) -> s5(synthesis) -> s5b(补洞 loop) -> s6(拼装) -> done', async () => {
|
|
65
|
+
const def = await parseWorkflowFile(wfPath);
|
|
66
|
+
const ids = def.steps.map((s) => s.id);
|
|
67
|
+
const expectOrder = [
|
|
68
|
+
'init',
|
|
69
|
+
's0_goal',
|
|
70
|
+
's0_pause',
|
|
71
|
+
's1_map',
|
|
72
|
+
's1_load_exit',
|
|
73
|
+
's1_exit_report',
|
|
74
|
+
's2_options',
|
|
75
|
+
's2_load_branches',
|
|
76
|
+
's2_assert_branches',
|
|
77
|
+
's3_deep_dive',
|
|
78
|
+
's4_verify',
|
|
79
|
+
's5_synthesis',
|
|
80
|
+
's5b_loop',
|
|
81
|
+
's5c_check_synthesis',
|
|
82
|
+
's5c_assert_synthesis',
|
|
83
|
+
's6_build_report',
|
|
84
|
+
's6_package',
|
|
85
|
+
's6_assert_report',
|
|
86
|
+
'done',
|
|
87
|
+
];
|
|
88
|
+
for (const id of expectOrder) expect(ids).toContain(id);
|
|
89
|
+
// 顺序
|
|
90
|
+
for (let i = 1; i < expectOrder.length; i++) {
|
|
91
|
+
expect(ids.indexOf(expectOrder[i - 1])).toBeLessThan(ids.indexOf(expectOrder[i]));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('early-exit 双路径:s1_exit_report 在 _early_exit==true 时补齐全套文件;主阶段以 when=_early_exit != true 守卫跳过', async () => {
|
|
96
|
+
const def = await parseWorkflowFile(wfPath);
|
|
97
|
+
const byId = new Map(def.steps.map((s) => [s.id, s]));
|
|
98
|
+
|
|
99
|
+
const exitReport = byId.get('s1_exit_report');
|
|
100
|
+
expect(exitReport?.when).toBe('_early_exit == true');
|
|
101
|
+
// 简化路径必须补齐 s6 拼装所需的全部文件(done_when 锁定)
|
|
102
|
+
const dw = String(exitReport?.done_when);
|
|
103
|
+
for (const f of ['03-options.md', 'branches.json', '05-verification.md', '06-synthesis.md', '07-learning-debt.md', 'gaps.json']) {
|
|
104
|
+
expect(dw).toContain(f);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 主阶段守卫:s2 / s4 / s5 / s5b 在 early-exit 时跳过
|
|
108
|
+
for (const id of ['s2_options', 's4_verify', 's5_synthesis', 's5b_loop']) {
|
|
109
|
+
expect(byId.get(id)?.when).toBe('_early_exit != true');
|
|
110
|
+
}
|
|
111
|
+
// assert 只在正常路径执行(early 路径 branches=[] 天然放行,无需弱化条件)
|
|
112
|
+
expect(byId.get('s2_assert_branches')?.when).toBe('_early_exit != true');
|
|
113
|
+
expect(String(byId.get('s2_assert_branches')?.condition)).toBe('length(branches) > 0');
|
|
114
|
+
// s1_map 的完成闸门包含 early-exit.json
|
|
115
|
+
expect(String(byId.get('s1_map')?.done_when)).toContain('early-exit.json');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('阶段0 零搜索(设计文档红线):allowed_tools 不含 WebSearch/WebFetch', async () => {
|
|
119
|
+
const def = await parseWorkflowFile(wfPath);
|
|
120
|
+
const s0 = def.steps.find((s) => s.id === 's0_goal');
|
|
121
|
+
const tools = (s0?.allowed_tools ?? []) as string[];
|
|
122
|
+
expect(tools).not.toContain('WebSearch');
|
|
123
|
+
expect(tools).not.toContain('WebFetch');
|
|
124
|
+
expect(tools).toEqual(['Read', 'Write']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('s5c 形式校验:确定性检查综合报告含必备章节(不依赖 agent 自觉)', async () => {
|
|
128
|
+
const def = await parseWorkflowFile(wfPath);
|
|
129
|
+
const check = def.steps.find((s) => s.id === 's5c_check_synthesis');
|
|
130
|
+
expect(check?.tool).toBe('Bash');
|
|
131
|
+
expect(String((check?.args as any)?.command)).toContain('grep');
|
|
132
|
+
|
|
133
|
+
const gate = def.steps.find((s) => s.id === 's5c_assert_synthesis');
|
|
134
|
+
expect(gate?.type).toBe('assert');
|
|
135
|
+
expect(String(gate?.condition)).toContain('核心结论');
|
|
136
|
+
expect(String(gate?.condition)).toContain('推荐方案');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('s3_deep_dive:for-each loop over branches、fresh_context、done_when 用 + 拼路径', async () => {
|
|
140
|
+
const def = await parseWorkflowFile(wfPath);
|
|
141
|
+
const loop = def.steps.find((s) => s.id === 's3_deep_dive');
|
|
142
|
+
expect(loop?.type).toBe('loop');
|
|
143
|
+
expect(String(loop?.over)).toContain('branches');
|
|
144
|
+
expect(loop?.as).toBe('branch');
|
|
145
|
+
expect(loop?.fresh_context).toBe(true);
|
|
146
|
+
|
|
147
|
+
const dive = ((loop?.do ?? []) as StepDef[]).find((s) => s.id === 's3_dive_one');
|
|
148
|
+
expect(dive?.type).toBeUndefined(); // 不是控制流,是 agent 步
|
|
149
|
+
expect(typeof dive?.agent).toBe('string');
|
|
150
|
+
// allowed_tools 必须限制为只读调研工具
|
|
151
|
+
expect(dive?.allowed_tools).toEqual(
|
|
152
|
+
expect.arrayContaining(['WebSearch', 'WebFetch', 'Read', 'Write']),
|
|
153
|
+
);
|
|
154
|
+
// done_when 用 + 拼接路径
|
|
155
|
+
expect(String(dive?.done_when)).toContain('branch.safe_name');
|
|
156
|
+
expect(String(dive?.done_when)).toContain('+');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('s5b_loop:while 模式(无 over)+ max_iterations=${inputs.max_iterations} + branch condition 判定缺口', async () => {
|
|
160
|
+
const def = await parseWorkflowFile(wfPath);
|
|
161
|
+
const loop = def.steps.find((s) => s.id === 's5b_loop');
|
|
162
|
+
expect(loop?.type).toBe('loop');
|
|
163
|
+
expect(loop?.over).toBeUndefined(); // while 模式
|
|
164
|
+
expect(String(loop?.max_iterations)).toContain('inputs.max_iterations');
|
|
165
|
+
|
|
166
|
+
const inner = (loop?.do ?? []) as StepDef[];
|
|
167
|
+
const brk = inner.find((s) => s.id === 's5b_break_if_empty');
|
|
168
|
+
expect(brk?.type).toBe('branch');
|
|
169
|
+
expect(String(brk?.condition)).toContain('length(_gaps)');
|
|
170
|
+
// then 里有 break
|
|
171
|
+
expect(((brk?.then ?? []) as StepDef[]).some((s) => s.type === 'break')).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('s0_pause:真 HITL 闸门(free_text=approve,用户输入作为补充要求贯穿)', async () => {
|
|
175
|
+
const def = await parseWorkflowFile(wfPath);
|
|
176
|
+
const pause = def.steps.find((s) => s.id === 's0_pause');
|
|
177
|
+
expect(pause?.type).toBe('pause');
|
|
178
|
+
expect(pause?.free_text).toBe('approve');
|
|
179
|
+
expect(typeof pause?.prompt).toBe('string');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('s6_build_report:确定性 Bash 拼装 out/research/${session}/deliverable/report.md(不烧 LLM),sec 降级标题,s6_package 精选脚本', async () => {
|
|
183
|
+
const def = await parseWorkflowFile(wfPath);
|
|
184
|
+
const build = def.steps.find((s) => s.id === 's6_build_report');
|
|
185
|
+
expect(build?.tool).toBe('Bash');
|
|
186
|
+
const cmd = String((build?.args as any)?.command);
|
|
187
|
+
// Phase 3:产物路径按 session 隔离(${session} 占位符由引擎展开)
|
|
188
|
+
expect(cmd).toContain('out/research/${session}/deliverable/report.md');
|
|
189
|
+
expect(cmd).toContain('sec out/research/${session}/01-goal.md');
|
|
190
|
+
expect(cmd).toContain('04-deep-dive');
|
|
191
|
+
expect(cmd).toContain("sed -E 's/^(#+) /\\1## /'");
|
|
192
|
+
expect((build?.artifacts ?? [])?.length).toBeGreaterThan(0);
|
|
193
|
+
|
|
194
|
+
const pkg = def.steps.find((s) => s.id === 's6_package');
|
|
195
|
+
expect(typeof pkg?.agent).toBe('string');
|
|
196
|
+
expect(String(pkg?.done_when)).toContain('deliverable/scripts/README.md');
|
|
197
|
+
|
|
198
|
+
const gate = def.steps.find((s) => s.id === 's6_assert_report');
|
|
199
|
+
expect(String(gate?.condition)).toContain('out/research/${session}/deliverable/report.md');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('s6 Bash 转义与注入防线:\\$ 经引擎插值还原为 $(Bash 变量可展开);自由文本 topic 不进命令体', async () => {
|
|
203
|
+
const def = await parseWorkflowFile(wfPath);
|
|
204
|
+
const build = def.steps.find((s) => s.id === 's6_build_report');
|
|
205
|
+
const rawCmd = String((build?.args as any)?.command);
|
|
206
|
+
|
|
207
|
+
// 注入防线:自由文本 inputs.topic 不得出现在任何 Bash 命令体里(enum 白名单的 depth 可以)
|
|
208
|
+
expect(rawCmd).not.toContain('${inputs.topic}');
|
|
209
|
+
const initCmd = String(
|
|
210
|
+
((def.steps.find((s) => s.id === 'init')?.args as any) ?? {}).command,
|
|
211
|
+
);
|
|
212
|
+
expect(initCmd).not.toContain('${inputs.topic}');
|
|
213
|
+
|
|
214
|
+
// 转义还原:引擎 interpolate 把 \$ 还原为 $ 后再交给 Bash --
|
|
215
|
+
// \$f/\$OUT 必须被还原(否则 Bash 拿到字面 $f 文件名,deep-dive 拼装会静默空白)
|
|
216
|
+
const vars = new VarStack();
|
|
217
|
+
vars.setGlobal('inputs', { topic: 'T', depth: 'standard', max_iterations: 2 });
|
|
218
|
+
const out = interpolate(rawCmd, vars);
|
|
219
|
+
expect(out).not.toContain('\\$');
|
|
220
|
+
expect(out).toContain('$OUT');
|
|
221
|
+
expect(out).toContain('$f');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('所有 tool: Bash 步骤的命令体都不触发 permanentDeleteGuard(包括注释里的示例代码)', async () => {
|
|
225
|
+
const def = await parseWorkflowFile(wfPath);
|
|
226
|
+
const bashSteps = def.steps.filter(
|
|
227
|
+
(s) => (s as any).tool === 'Bash',
|
|
228
|
+
);
|
|
229
|
+
expect(bashSteps.length).toBeGreaterThan(0);
|
|
230
|
+
|
|
231
|
+
// 逐行匹配 permanentDeleteGuard 的 BASH_DELETE 正则:
|
|
232
|
+
// /(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i
|
|
233
|
+
// /(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i
|
|
234
|
+
const delPatterns = [
|
|
235
|
+
/(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
|
|
236
|
+
/(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
|
|
237
|
+
];
|
|
238
|
+
for (const step of bashSteps) {
|
|
239
|
+
const cmd = String(((step as any).args as any).command ?? '');
|
|
240
|
+
for (const pat of delPatterns) {
|
|
241
|
+
if (pat.test(cmd)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`步骤 ${step.id} 的 Bash 命令体命中了 permanentDeleteGuard 正则 ${pat.toString()}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('所有 ${} 模板插值和裸表达式都能被引擎求值(不抛错)', async () => {
|
|
251
|
+
const def = await parseWorkflowFile(wfPath);
|
|
252
|
+
const vars = new VarStack();
|
|
253
|
+
vars.setGlobal('inputs', {
|
|
254
|
+
topic: 'Rust 异步运行时',
|
|
255
|
+
depth: 'standard',
|
|
256
|
+
max_iterations: 2,
|
|
257
|
+
});
|
|
258
|
+
vars.setGlobal('branches', [
|
|
259
|
+
{ name: 'Tokio', safe_name: 'tokio', description: '主流' },
|
|
260
|
+
{ name: 'async-std', safe_name: 'async-std', description: '另一个' },
|
|
261
|
+
]);
|
|
262
|
+
vars.setGlobal('_gaps', []);
|
|
263
|
+
vars.setGlobal('_init_out', 'init_ok');
|
|
264
|
+
vars.setGlobal('session', 'default');
|
|
265
|
+
vars.setGlobal('_report_out', 'ok:out/research/${session}/deliverable/report.md');
|
|
266
|
+
vars.setIteration('branch', { name: 'Tokio', safe_name: 'tokio', description: '主流' });
|
|
267
|
+
vars.setIteration('branch_idx', 0);
|
|
268
|
+
vars.setIteration('_iter', 0);
|
|
269
|
+
vars.setIteration('_iter_idx', 0);
|
|
270
|
+
|
|
271
|
+
// 模板插值
|
|
272
|
+
for (const s of allStrings(def)) {
|
|
273
|
+
if (!s.includes('${')) continue;
|
|
274
|
+
expect(() => interpolate(s, vars)).not.toThrow(
|
|
275
|
+
`字符串插值失败: ${s.slice(0, 200)}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 裸表达式:when / condition / over / done_when / set 值 / max_iterations
|
|
280
|
+
const naked: Array<[string, string]> = [];
|
|
281
|
+
for (const s of allStepsRec(def.steps)) {
|
|
282
|
+
if (s.when) naked.push([`${s.id}.when`, s.when]);
|
|
283
|
+
if (s.condition) naked.push([`${s.id}.condition`, s.condition]);
|
|
284
|
+
if (typeof s.over === 'string') naked.push([`${s.id}.over`, s.over]);
|
|
285
|
+
if (s.done_when) naked.push([`${s.id}.done_when`, s.done_when]);
|
|
286
|
+
if (typeof s.max_iterations === 'string')
|
|
287
|
+
naked.push([`${s.id}.max_iterations`, s.max_iterations]);
|
|
288
|
+
for (const [k, v] of Object.entries(s.set ?? {})) {
|
|
289
|
+
if (typeof v === 'string' && v.startsWith('${') && v.endsWith('}')) {
|
|
290
|
+
naked.push([`${s.id}.set.${k}`, v.slice(2, -1)]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const [loc, expr] of naked) {
|
|
295
|
+
expect(() => evalExpr(expr, vars)).not.toThrow(
|
|
296
|
+
`表达式失败 [${loc}]: ${expr}`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('补洞收敛可见化:s5b_final_gaps 恒执行(无 when),done 按剩余缺口数分流提示', async () => {
|
|
302
|
+
const def = await parseWorkflowFile(wfPath);
|
|
303
|
+
const steps = allStepsRec(def.steps);
|
|
304
|
+
|
|
305
|
+
// s5b_final_gaps 无 when 守卫:early-exit 路径也执行(gaps.json=[] 由 s1_exit_report 补齐),
|
|
306
|
+
// 保证 done 引用的 _remaining_gaps 恒有定义
|
|
307
|
+
const fg = steps.find((s) => s.id === 's5b_final_gaps');
|
|
308
|
+
expect(fg).toBeDefined();
|
|
309
|
+
expect(fg?.when).toBeUndefined();
|
|
310
|
+
expect(String(fg?.set?._remaining_gaps)).toContain("readJson('out/research/${session}/gaps.json')");
|
|
311
|
+
|
|
312
|
+
// done 消息按 _remaining_gaps 分流:>0 警告剩余缺口,=0 报告收敛
|
|
313
|
+
const done = steps.find((s) => s.id === 'done');
|
|
314
|
+
const msg = String(done?.message);
|
|
315
|
+
expect(msg).toContain('_remaining_gaps > 0');
|
|
316
|
+
expect(msg).toContain('缺口未补全');
|
|
317
|
+
expect(msg).toContain('已全部收敛');
|
|
318
|
+
|
|
319
|
+
// 三元插值在两种取值下都能求值(模拟 done 执行)
|
|
320
|
+
const vars = new VarStack();
|
|
321
|
+
vars.setGlobal('inputs', { topic: 'T', depth: 'light', max_iterations: 1 });
|
|
322
|
+
const ternary = '_remaining_gaps > 0 ? \'⚠️ 仍有 \' + _remaining_gaps + \' 条缺口\' : \'✅ 收敛\'';
|
|
323
|
+
vars.setGlobal('_remaining_gaps', 3);
|
|
324
|
+
expect(evalExpr(ternary, vars)).toContain('3 条缺口');
|
|
325
|
+
vars.setGlobal('_remaining_gaps', 0);
|
|
326
|
+
expect(evalExpr(ternary, vars)).toContain('收敛');
|
|
327
|
+
});
|
|
328
|
+
});
|