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
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
existsSync,
|
|
29
29
|
mkdirSync,
|
|
30
30
|
mkdtempSync,
|
|
31
|
+
readdirSync,
|
|
31
32
|
readFileSync,
|
|
32
33
|
rmSync,
|
|
33
34
|
writeFileSync,
|
|
@@ -36,6 +37,7 @@ import { tmpdir } from 'node:os';
|
|
|
36
37
|
import { join } from 'node:path';
|
|
37
38
|
|
|
38
39
|
import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.js';
|
|
40
|
+
import { createAgentRuntimeContext } from '../../../src/plugin-sdk.js';
|
|
39
41
|
import { ALL_TOOLS } from '../../../src/tools/index.js';
|
|
40
42
|
import { runWorkflow } from '../src/runner.js';
|
|
41
43
|
import type { LoopEvent, Provider } from '../../../src/types.js';
|
|
@@ -43,6 +45,43 @@ import type { RunContext, WorkflowDef, WorkflowEvent } from '../src/types.js';
|
|
|
43
45
|
|
|
44
46
|
type WfOrLoop = WorkflowEvent | LoopEvent;
|
|
45
47
|
|
|
48
|
+
const DEFAULT_SESSION = 'test-session';
|
|
49
|
+
|
|
50
|
+
function findInstanceDir(base: string, wfName: string, suffix = ''): string | null {
|
|
51
|
+
const wfDir = join(base, '.minimal-agent-workflow', wfName);
|
|
52
|
+
if (!existsSync(wfDir)) {
|
|
53
|
+
const legacyDir = suffix
|
|
54
|
+
? join(base, '.minimal-agent-workflow', wfName + suffix)
|
|
55
|
+
: wfDir;
|
|
56
|
+
if (existsSync(legacyDir)) return legacyDir;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Phase 3:session 化——实例目录名 = sessionId(默认 'default')
|
|
61
|
+
const sessionDir = suffix
|
|
62
|
+
? join(wfDir, DEFAULT_SESSION + suffix)
|
|
63
|
+
: join(wfDir, DEFAULT_SESSION);
|
|
64
|
+
if (existsSync(sessionDir)) return sessionDir;
|
|
65
|
+
|
|
66
|
+
// 旧布局兼容:wfDir 本身有 current.json(无子实例目录时判定为 legacy)
|
|
67
|
+
if (!suffix && existsSync(join(wfDir, 'current.json'))) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = readdirSync(wfDir, { withFileTypes: true });
|
|
70
|
+
const hasInstanceSubdir = entries.some(
|
|
71
|
+
(e) => e.isDirectory() && /^[0-9a-f]{16}$/.test(e.name),
|
|
72
|
+
);
|
|
73
|
+
if (!hasInstanceSubdir) return wfDir;
|
|
74
|
+
} catch {
|
|
75
|
+
return wfDir;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasInstance(base: string, wfName: string, suffix = ''): boolean {
|
|
82
|
+
return findInstanceDir(base, wfName, suffix) !== null;
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
const fakeProvider: Provider = {
|
|
47
86
|
name: 'test',
|
|
48
87
|
baseURL: '',
|
|
@@ -72,7 +111,13 @@ describe('runner::pre-flight fresh 模式', () => {
|
|
|
72
111
|
initWorkingDir: () => tmp,
|
|
73
112
|
_resetWorkingDir: () => {},
|
|
74
113
|
}));
|
|
75
|
-
ctx = {
|
|
114
|
+
ctx = {
|
|
115
|
+
provider: fakeProvider,
|
|
116
|
+
history: [],
|
|
117
|
+
cwd: tmp,
|
|
118
|
+
tools: [],
|
|
119
|
+
runtimeContext: createAgentRuntimeContext({ surface: 'workflow', profile: 'main', cwd: tmp, sessionId: DEFAULT_SESSION, owner: 'local-owner', interactiveAsk: false }),
|
|
120
|
+
};
|
|
76
121
|
});
|
|
77
122
|
|
|
78
123
|
afterEach(() => {
|
|
@@ -125,7 +170,13 @@ describe('runner::resume 与断点续执行', () => {
|
|
|
125
170
|
initWorkingDir: () => tmp,
|
|
126
171
|
_resetWorkingDir: () => {},
|
|
127
172
|
}));
|
|
128
|
-
ctx = {
|
|
173
|
+
ctx = {
|
|
174
|
+
provider: fakeProvider,
|
|
175
|
+
history: [],
|
|
176
|
+
cwd: tmp,
|
|
177
|
+
tools: ALL_TOOLS,
|
|
178
|
+
runtimeContext: createAgentRuntimeContext({ surface: 'workflow', profile: 'main', cwd: tmp, sessionId: DEFAULT_SESSION, owner: 'local-owner', interactiveAsk: false }),
|
|
179
|
+
};
|
|
129
180
|
});
|
|
130
181
|
|
|
131
182
|
afterEach(() => {
|
|
@@ -254,7 +305,13 @@ describe('runner::self_heal retry 机制', () => {
|
|
|
254
305
|
initWorkingDir: () => tmp,
|
|
255
306
|
_resetWorkingDir: () => {},
|
|
256
307
|
}));
|
|
257
|
-
ctx = {
|
|
308
|
+
ctx = {
|
|
309
|
+
provider: fakeProvider,
|
|
310
|
+
history: [],
|
|
311
|
+
cwd: tmp,
|
|
312
|
+
tools: [],
|
|
313
|
+
runtimeContext: createAgentRuntimeContext({ surface: 'workflow', profile: 'main', cwd: tmp, sessionId: DEFAULT_SESSION, owner: 'local-owner', interactiveAsk: false }),
|
|
314
|
+
};
|
|
258
315
|
});
|
|
259
316
|
|
|
260
317
|
afterEach(() => {
|
|
@@ -350,9 +407,9 @@ describe('runner::self_heal retry 机制', () => {
|
|
|
350
407
|
|
|
351
408
|
await collect(runWorkflow(def, {}, ctx));
|
|
352
409
|
|
|
353
|
-
// 成功后应 archive
|
|
354
|
-
expect(
|
|
355
|
-
expect(
|
|
410
|
+
// 成功后应 archive(活动实例目录改名 .done;workflow 级父目录保留)
|
|
411
|
+
expect(hasInstance(tmp, 'archive-test')).toBe(false);
|
|
412
|
+
expect(hasInstance(tmp, 'archive-test', '.done')).toBe(true);
|
|
356
413
|
});
|
|
357
414
|
|
|
358
415
|
it('失败路径 → 保留状态目录供排查', async () => {
|
|
@@ -367,12 +424,12 @@ describe('runner::self_heal retry 机制', () => {
|
|
|
367
424
|
|
|
368
425
|
await collect(runWorkflow(def, {}, ctx));
|
|
369
426
|
|
|
370
|
-
// 失败时应保留状态目录(
|
|
371
|
-
expect(
|
|
427
|
+
// 失败时应保留状态目录(workflow 级父目录 + 实例目录)
|
|
428
|
+
expect(hasInstance(tmp, 'keep-state')).toBe(true);
|
|
372
429
|
// progress.md 应包含失败步骤信息
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
const progress = readFileSync(
|
|
430
|
+
const instDir = findInstanceDir(tmp, 'keep-state');
|
|
431
|
+
if (instDir) {
|
|
432
|
+
const progress = readFileSync(join(instDir, 'progress.md'), 'utf8');
|
|
376
433
|
expect(progress).toContain('fail');
|
|
377
434
|
}
|
|
378
435
|
});
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
15
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
|
|
19
19
|
import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.js';
|
|
20
|
+
import { createAgentRuntimeContext } from '../../../src/plugin-sdk.js';
|
|
20
21
|
import { ALL_TOOLS } from '../../../src/tools/index.js';
|
|
21
22
|
import { runWorkflow } from '../src/runner.js';
|
|
22
23
|
import type { LoopEvent, Provider } from '../../../src/types.js';
|
|
@@ -41,6 +42,45 @@ async function collect(
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const STATE_DIR = '.minimal-agent-workflow';
|
|
45
|
+
// 集成测试统一用显式 sessionId(引擎对未指定 session 自动命名,见 runner 的
|
|
46
|
+
// generateDefaultSessionId;测目录生命周期需要可预测的 session 名)
|
|
47
|
+
const SESSION = 'test-session';
|
|
48
|
+
|
|
49
|
+
function findInstanceDir(base: string, wfName: string, suffix = ''): string | null {
|
|
50
|
+
const wfDir = join(base, STATE_DIR, wfName);
|
|
51
|
+
if (!existsSync(wfDir)) {
|
|
52
|
+
// 旧布局兼容:wfDir 本身就是实例目录(wfName.done 形式)
|
|
53
|
+
const legacyDir = suffix
|
|
54
|
+
? join(base, STATE_DIR, wfName + suffix)
|
|
55
|
+
: wfDir;
|
|
56
|
+
if (existsSync(legacyDir)) return legacyDir;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Phase 3:session 化——实例目录名 = sessionId
|
|
61
|
+
const sessionDir = suffix
|
|
62
|
+
? join(wfDir, SESSION + suffix)
|
|
63
|
+
: join(wfDir, SESSION);
|
|
64
|
+
if (existsSync(sessionDir)) return sessionDir;
|
|
65
|
+
|
|
66
|
+
// 旧布局兼容:wfDir 本身有 current.json(无子实例目录时判定为 legacy)
|
|
67
|
+
if (!suffix && existsSync(join(wfDir, 'current.json'))) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = readdirSync(wfDir, { withFileTypes: true });
|
|
70
|
+
const hasInstanceSubdir = entries.some(
|
|
71
|
+
(e) => e.isDirectory() && /^[0-9a-f]{16}$/.test(e.name),
|
|
72
|
+
);
|
|
73
|
+
if (!hasInstanceSubdir) return wfDir;
|
|
74
|
+
} catch {
|
|
75
|
+
return wfDir;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasInstance(base: string, wfName: string, suffix = ''): boolean {
|
|
82
|
+
return findInstanceDir(base, wfName, suffix) !== null;
|
|
83
|
+
}
|
|
44
84
|
|
|
45
85
|
describe('workflow state-dir 生命周期', () => {
|
|
46
86
|
let tmp: string;
|
|
@@ -55,7 +95,14 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
55
95
|
initWorkingDir: () => tmp,
|
|
56
96
|
_resetWorkingDir: () => {},
|
|
57
97
|
}));
|
|
58
|
-
ctx = {
|
|
98
|
+
ctx = {
|
|
99
|
+
provider: fakeProvider,
|
|
100
|
+
history: [],
|
|
101
|
+
cwd: tmp,
|
|
102
|
+
tools: ALL_TOOLS,
|
|
103
|
+
// 显式 sessionId:目录名可预测,resume/归档断言稳定
|
|
104
|
+
runtimeContext: createAgentRuntimeContext({ surface: 'workflow', profile: 'main', cwd: tmp, sessionId: SESSION, owner: 'local-owner', interactiveAsk: false }),
|
|
105
|
+
};
|
|
59
106
|
});
|
|
60
107
|
|
|
61
108
|
afterEach(() => {
|
|
@@ -73,8 +120,8 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
73
120
|
const events = await collect(runWorkflow(def, {}, ctx));
|
|
74
121
|
expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
|
|
75
122
|
// 成功路径:活动目录被归档改名(completed 检测靠它),不留活动现场
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
123
|
+
expect(hasInstance(tmp, 'ok')).toBe(false);
|
|
124
|
+
expect(hasInstance(tmp, 'ok', '.done')).toBe(true);
|
|
78
125
|
});
|
|
79
126
|
|
|
80
127
|
it('halt → 保留状态目录 + progress.md 含失败那步', async () => {
|
|
@@ -91,9 +138,10 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
91
138
|
expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
|
|
92
139
|
|
|
93
140
|
// 失败现场必须保留,供事后排查
|
|
94
|
-
const stateDir =
|
|
95
|
-
expect(
|
|
96
|
-
|
|
141
|
+
const stateDir = findInstanceDir(tmp, 'boom');
|
|
142
|
+
expect(stateDir).not.toBeNull();
|
|
143
|
+
expect(stateDir).toBeDefined();
|
|
144
|
+
const progress = readFileSync(join(stateDir as string, 'progress.md'), 'utf8');
|
|
97
145
|
// 失败那步的 ✗ 行 + onFail 文案都应在 progress 里
|
|
98
146
|
expect(progress).toContain('will_fail');
|
|
99
147
|
expect(progress).toContain('nope');
|
|
@@ -106,7 +154,7 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
106
154
|
steps: [{ id: 'f', type: 'assert', condition: 'false', onFail: 'x' }],
|
|
107
155
|
};
|
|
108
156
|
await collect(runWorkflow(failDef, {}, ctx));
|
|
109
|
-
expect(
|
|
157
|
+
expect(hasInstance(tmp, 'rerun')).toBe(true); // 失败留下了
|
|
110
158
|
|
|
111
159
|
// 第二次成功跑(同名同 inputs = 同任务 resume):成功后归档 → 活动目录干净
|
|
112
160
|
const okDef: WorkflowDef = {
|
|
@@ -115,8 +163,8 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
115
163
|
steps: [{ id: 'ok', type: 'assert', condition: 'true' }],
|
|
116
164
|
};
|
|
117
165
|
await collect(runWorkflow(okDef, {}, ctx));
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
166
|
+
expect(hasInstance(tmp, 'rerun')).toBe(false); // 不堆积
|
|
167
|
+
expect(hasInstance(tmp, 'rerun', '.done')).toBe(true);
|
|
120
168
|
});
|
|
121
169
|
|
|
122
170
|
it('E6:A 工作流失败现场不被不同名工作流 B 的运行破坏(按名隔离)', async () => {
|
|
@@ -131,7 +179,7 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
131
179
|
],
|
|
132
180
|
};
|
|
133
181
|
await collect(runWorkflow(aDef, {}, ctx));
|
|
134
|
-
expect(
|
|
182
|
+
expect(hasInstance(tmp, 'wf-a')).toBe(true);
|
|
135
183
|
expect(existsSync(join(tmp, 'A-PLAN.md'))).toBe(true);
|
|
136
184
|
|
|
137
185
|
// 跑不同名工作流 B(曾经会清掉 A 的状态目录和产物——E6 事故)
|
|
@@ -144,7 +192,7 @@ describe('workflow state-dir 生命周期', () => {
|
|
|
144
192
|
expect(evB.some((e) => e.type === 'workflow_done')).toBe(true);
|
|
145
193
|
|
|
146
194
|
// A 的现场与产物完好,resume 能力保留
|
|
147
|
-
expect(
|
|
195
|
+
expect(hasInstance(tmp, 'wf-a')).toBe(true);
|
|
148
196
|
expect(existsSync(join(tmp, 'A-PLAN.md'))).toBe(true);
|
|
149
197
|
});
|
|
150
198
|
});
|
|
@@ -24,6 +24,7 @@ import { tmpdir } from 'node:os';
|
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
|
|
26
26
|
import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.js';
|
|
27
|
+
import { createAgentRuntimeContext } from '../../../src/plugin-sdk.js';
|
|
27
28
|
import type { LoopEvent, Provider } from '../../../src/types.js';
|
|
28
29
|
import {
|
|
29
30
|
buildTaskArbitrator,
|
|
@@ -185,11 +186,13 @@ describe('workflowState::init × 仲裁(resume 保留现场 / new 清理重开
|
|
|
185
186
|
});
|
|
186
187
|
|
|
187
188
|
it('仲裁判 resume → 保留旧状态与产物、回放 checkpoint、带 prevGoal/supplementaryGoal', async () => {
|
|
188
|
-
// 旧任务:fresh + 完成一步 +
|
|
189
|
+
// 旧任务:fresh + 完成一步 + 留产物 + 留补充要求(不 markDone = 未完成)
|
|
189
190
|
const s1 = new WorkflowState(tmp, 'plan');
|
|
190
191
|
await s1.init('plan', { goal: '写产品架构 PPT' }, STEPS, 1, ['PLAN.md']);
|
|
191
192
|
await s1.recordCheckpoint('s1', 'ok', { out: 'v1' }, {});
|
|
193
|
+
await s1.appendSupplement('用户要求加一章');
|
|
192
194
|
writeFileSync(join(tmp, 'PLAN.md'), '# 半成品');
|
|
195
|
+
const oldDir = s1.dir;
|
|
193
196
|
|
|
194
197
|
// 新输入「继续」(hash 必然不等)+ 仲裁器判 resume
|
|
195
198
|
const s2 = new WorkflowState(tmp, 'plan');
|
|
@@ -202,6 +205,10 @@ describe('workflowState::init × 仲裁(resume 保留现场 / new 清理重开
|
|
|
202
205
|
expect(r.supplementaryGoal).toBe('继续');
|
|
203
206
|
// 现场未被清理
|
|
204
207
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(true);
|
|
208
|
+
// 回归:resume 后实例目录必须仍指向旧任务(否则 supplements / checkpoint 全写丢,
|
|
209
|
+
// 断点续跑静默失效——曾因 finally 误把 this.dir 改回新 hash 目录)
|
|
210
|
+
expect(s2.dir).toBe(oldDir);
|
|
211
|
+
expect(await s2.readSupplements()).toEqual(['用户要求加一章']);
|
|
205
212
|
});
|
|
206
213
|
|
|
207
214
|
it('仲裁判 new → 走 new-task 清理重开', async () => {
|
|
@@ -257,7 +264,9 @@ describe('workflowState::init × 仲裁(resume 保留现场 / new 清理重开
|
|
|
257
264
|
expect(r.mode).toBe('fresh');
|
|
258
265
|
expect(arbCalled).toBe(0);
|
|
259
266
|
// A 的现场完好(曾经会被 B 的 new-task 清掉——E6 事故)
|
|
260
|
-
|
|
267
|
+
// 新布局:current.json 在实例子目录里;旧布局兼容时直接在 wfDir 下
|
|
268
|
+
const aDir = sA.dir;
|
|
269
|
+
expect(existsSync(join(aDir, 'current.json'))).toBe(true);
|
|
261
270
|
});
|
|
262
271
|
});
|
|
263
272
|
|
|
@@ -276,7 +285,14 @@ describe('runWorkflow × 仲裁延续(端到端)', () => {
|
|
|
276
285
|
initWorkingDir: () => tmp,
|
|
277
286
|
_resetWorkingDir: () => {},
|
|
278
287
|
}));
|
|
279
|
-
ctx = {
|
|
288
|
+
ctx = {
|
|
289
|
+
provider: fakeProvider,
|
|
290
|
+
history: [],
|
|
291
|
+
cwd: tmp,
|
|
292
|
+
tools: [],
|
|
293
|
+
// 显式 sessionId:两次 runWorkflow 同 session,仲裁「继续」才能 resume 旧任务
|
|
294
|
+
runtimeContext: createAgentRuntimeContext({ surface: 'workflow', profile: 'main', cwd: tmp, sessionId: 'test-session', owner: 'local-owner', interactiveAsk: false }),
|
|
295
|
+
};
|
|
280
296
|
});
|
|
281
297
|
afterEach(() => {
|
|
282
298
|
delete process.env.MINIMAL_AGENT_CWD;
|
|
@@ -67,9 +67,10 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
67
67
|
writeFileSync(join(tmp, 'PLAN.md'), '# plan');
|
|
68
68
|
await s1.markDone();
|
|
69
69
|
await s1.archive();
|
|
70
|
-
//
|
|
71
|
-
expect(existsSync(join(tmp, '.minimal-agent-workflow', 'plan'))).toBe(
|
|
72
|
-
expect(existsSync(
|
|
70
|
+
// 归档后活动实例被改名 .done(workflow 级父目录保留——里面可能有多个实例)
|
|
71
|
+
expect(existsSync(join(tmp, '.minimal-agent-workflow', 'plan'))).toBe(true);
|
|
72
|
+
expect(existsSync(s1.dir + '.done')).toBe(true);
|
|
73
|
+
expect(existsSync(s1.dir)).toBe(false);
|
|
73
74
|
|
|
74
75
|
// 第二次:同 goal 重跑 → completed(经 .done 检测),报真实产物
|
|
75
76
|
const s2 = new WorkflowState(tmp, 'plan');
|
|
@@ -104,8 +105,9 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
104
105
|
const s2 = new WorkflowState(tmp, 'plan');
|
|
105
106
|
const r2 = await s2.init('plan', { goal: '完全不相关的目标B' }, STEPS, 1, ['PLAN.md']);
|
|
106
107
|
expect(r2.mode).toBe('new-task');
|
|
107
|
-
expect(r2.cleanedArtifacts).toContain('PLAN.md');
|
|
108
108
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
109
|
+
expect(r2.archivedTo).toBeDefined();
|
|
110
|
+
expect(existsSync(join(tmp, r2.archivedTo as string, 'PLAN.md'))).toBe(true);
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
it('legacy 状态文件(无 artifacts 字段)+ 同名 workflow 换 goal → new-task 用本次声明清单兜底清理', async () => {
|
|
@@ -128,8 +130,9 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
128
130
|
const r = await s.init('plan', { goal: '全新目标' }, STEPS, 1, ['PLAN.md']);
|
|
129
131
|
expect(r.mode).toBe('new-task');
|
|
130
132
|
// 兜底清理生效:同名 workflow 的声明产物语义稳定,按本次声明清单清掉残留
|
|
131
|
-
expect(r.cleanedArtifacts).toContain('PLAN.md');
|
|
132
133
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
134
|
+
expect(r.archivedTo).toBeDefined();
|
|
135
|
+
expect(existsSync(join(tmp, r.archivedTo as string, 'PLAN.md'))).toBe(true);
|
|
133
136
|
});
|
|
134
137
|
|
|
135
138
|
it('/new 后孤儿产物:无任何状态但声明产物已存在 → fresh 启动时清理(全新任务从干净状态开始)', async () => {
|
|
@@ -141,8 +144,59 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
141
144
|
const r = await s.init('plan', { goal: '目标A' }, STEPS, 1, ['PLAN.md']);
|
|
142
145
|
expect(r.mode).toBe('fresh');
|
|
143
146
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
144
|
-
|
|
145
|
-
expect(r.
|
|
147
|
+
expect(r.archivedTo).toBeDefined();
|
|
148
|
+
expect(existsSync(join(tmp, r.archivedTo as string, 'PLAN.md'))).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('A4:归档保留相对路径结构,只归档声明产物、不碰未声明文件', async () => {
|
|
152
|
+
const { mkdirSync } = await import('node:fs');
|
|
153
|
+
mkdirSync(join(tmp, 'out'), { recursive: true });
|
|
154
|
+
const s1 = new WorkflowState(tmp, 'plan');
|
|
155
|
+
await s1.init('plan', { goal: '目标A' }, STEPS, 1, ['out/PLAN.md']);
|
|
156
|
+
writeFileSync(join(tmp, 'out', 'PLAN.md'), '# A');
|
|
157
|
+
writeFileSync(join(tmp, 'out', 'USER-OWN.md'), '# 用户自己的文件');
|
|
158
|
+
|
|
159
|
+
const s2 = new WorkflowState(tmp, 'plan');
|
|
160
|
+
const r2 = await s2.init('plan', { goal: '目标B' }, STEPS, 1, ['out/PLAN.md']);
|
|
161
|
+
expect(r2.mode).toBe('new-task');
|
|
162
|
+
expect(existsSync(join(tmp, 'out', 'PLAN.md'))).toBe(false);
|
|
163
|
+
expect(existsSync(join(tmp, 'out', 'USER-OWN.md'))).toBe(true);
|
|
164
|
+
expect(existsSync(join(tmp, r2.archivedTo as string, 'out', 'PLAN.md'))).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('B8:声明产物支持 glob(out/*.md),new-task 清理时展开为实际文件归档', async () => {
|
|
168
|
+
const { mkdirSync } = await import('node:fs');
|
|
169
|
+
mkdirSync(join(tmp, 'out'), { recursive: true });
|
|
170
|
+
const s1 = new WorkflowState(tmp, 'research');
|
|
171
|
+
await s1.init('research', { goal: '目标A' }, STEPS, 1, ['out/deep-dive-*.md']);
|
|
172
|
+
writeFileSync(join(tmp, 'out', 'deep-dive-a.md'), '# A');
|
|
173
|
+
writeFileSync(join(tmp, 'out', 'deep-dive-b.md'), '# B');
|
|
174
|
+
writeFileSync(join(tmp, 'out', 'notes.txt'), '# 不匹配 md');
|
|
175
|
+
|
|
176
|
+
const s2 = new WorkflowState(tmp, 'research');
|
|
177
|
+
const r2 = await s2.init('research', { goal: '目标B' }, STEPS, 1, ['out/deep-dive-*.md']);
|
|
178
|
+
expect(r2.mode).toBe('new-task');
|
|
179
|
+
expect(existsSync(join(tmp, 'out', 'deep-dive-a.md'))).toBe(false);
|
|
180
|
+
expect(existsSync(join(tmp, 'out', 'deep-dive-b.md'))).toBe(false);
|
|
181
|
+
expect(existsSync(join(tmp, 'out', 'notes.txt'))).toBe(true);
|
|
182
|
+
const trash = join(tmp, r2.archivedTo as string);
|
|
183
|
+
expect(existsSync(join(trash, 'out', 'deep-dive-a.md'))).toBe(true);
|
|
184
|
+
expect(existsSync(join(trash, 'out', 'deep-dive-b.md'))).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('B8:越界 glob(../x*)不展开不清理(fail-closed)', async () => {
|
|
188
|
+
writeFileSync(join(tmp, 'PLAN.md'), '# inside');
|
|
189
|
+
const outside = mkdtempSync(join(tmpdir(), 'wf-outside-'));
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(join(outside, 'secret.md'), '# outside');
|
|
192
|
+
const s = new WorkflowState(tmp, 'plan');
|
|
193
|
+
const r = await s.init('plan', { goal: '目标A' }, STEPS, 1, ['../x*.md', 'PLAN.md']);
|
|
194
|
+
expect(r.mode).toBe('fresh');
|
|
195
|
+
expect(existsSync(join(outside, 'secret.md'))).toBe(true);
|
|
196
|
+
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
197
|
+
} finally {
|
|
198
|
+
rmSync(outside, { recursive: true, force: true });
|
|
199
|
+
}
|
|
146
200
|
});
|
|
147
201
|
|
|
148
202
|
it('resume 恢复点用最后完成步的【索引】而非 checkpoint 计数(when 跳过步不再被重复执行)', async () => {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ALL_TOOLS,
|
|
5
|
+
type LoopEvent,
|
|
6
|
+
type Provider,
|
|
7
|
+
type ToolCallContext,
|
|
8
|
+
type ToolResult,
|
|
9
|
+
createAgentRuntimeContext,
|
|
10
|
+
createSessionState,
|
|
11
|
+
} from '../../../src/plugin-sdk.js';
|
|
12
|
+
import { execToolStep } from '../src/stepExecutors/tool.js';
|
|
13
|
+
import { type RunContext, type StepResult, VarStack } from '../src/types.js';
|
|
14
|
+
|
|
15
|
+
const provider: Provider = {
|
|
16
|
+
name: 'workflow-tool-test',
|
|
17
|
+
baseURL: '',
|
|
18
|
+
apiKey: '',
|
|
19
|
+
model: 'fixture',
|
|
20
|
+
contextWindow: 128_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function collect(
|
|
24
|
+
generator: AsyncGenerator<LoopEvent, StepResult, void>,
|
|
25
|
+
): Promise<{ events: LoopEvent[]; result: StepResult }> {
|
|
26
|
+
const events: LoopEvent[] = [];
|
|
27
|
+
while (true) {
|
|
28
|
+
const next = await generator.next();
|
|
29
|
+
if (next.done) return { events, result: next.value };
|
|
30
|
+
events.push(next.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeContext(
|
|
35
|
+
executeTool: NonNullable<RunContext['executeTool']>,
|
|
36
|
+
): RunContext {
|
|
37
|
+
const sessionState = createSessionState();
|
|
38
|
+
sessionState.activeWorktree = {
|
|
39
|
+
id: 'wt_test',
|
|
40
|
+
path: '/tmp/workflow-active-worktree',
|
|
41
|
+
branch: 'workflow/test',
|
|
42
|
+
baseSha: 'deadbeef',
|
|
43
|
+
gitRoot: '/tmp/workflow-root',
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
provider,
|
|
47
|
+
history: [],
|
|
48
|
+
cwd: '/tmp/workflow-root',
|
|
49
|
+
tools: ALL_TOOLS,
|
|
50
|
+
sessionState,
|
|
51
|
+
runtimeContext: createAgentRuntimeContext({
|
|
52
|
+
surface: 'workflow',
|
|
53
|
+
profile: 'main',
|
|
54
|
+
cwd: '/tmp/workflow-root',
|
|
55
|
+
workspaceId: 'workspace_test',
|
|
56
|
+
sessionId: 'session_test',
|
|
57
|
+
owner: 'local-owner',
|
|
58
|
+
memoryAccess: 'read-only',
|
|
59
|
+
interactiveAsk: false,
|
|
60
|
+
}),
|
|
61
|
+
promptCache: {
|
|
62
|
+
scopeKey: 'workflow-test-scope',
|
|
63
|
+
conversationKey: 'workflow-test-conversation',
|
|
64
|
+
},
|
|
65
|
+
executeTool,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('workflow deterministic tool runtime', () => {
|
|
70
|
+
test('all registered tools use the same generic deterministic node dispatcher', async () => {
|
|
71
|
+
const dispatched: string[] = [];
|
|
72
|
+
const ctx = makeContext(async (name) => {
|
|
73
|
+
dispatched.push(name);
|
|
74
|
+
return { ok: true, content: `${name} ok` };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
for (const tool of ALL_TOOLS) {
|
|
78
|
+
const { result } = await collect(
|
|
79
|
+
execToolStep(
|
|
80
|
+
{ id: `catalog_${tool.name}`, tool: tool.name, args: {} },
|
|
81
|
+
new VarStack(),
|
|
82
|
+
ctx,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
expect(result.raw).toMatchObject({ content: `${tool.name} ok` });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(dispatched).toEqual(ALL_TOOLS.map((tool) => tool.name));
|
|
89
|
+
expect(new Set(dispatched).size).toBe(ALL_TOOLS.length);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('passes canonical runtime context and follows the active worktree cwd', async () => {
|
|
93
|
+
let captured: ToolCallContext | undefined;
|
|
94
|
+
const ctx = makeContext(async (_name, _args, _signal, context) => {
|
|
95
|
+
captured = context;
|
|
96
|
+
return { ok: true, content: 'ok' };
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await collect(
|
|
100
|
+
execToolStep(
|
|
101
|
+
{ id: 'context', tool: 'Read', args: { file_path: 'README.md' } },
|
|
102
|
+
new VarStack(),
|
|
103
|
+
ctx,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(captured).toBeDefined();
|
|
108
|
+
expect(captured?.provider).toBe(provider);
|
|
109
|
+
expect(captured?.history).toBe(ctx.history);
|
|
110
|
+
expect(captured?.sessionState).toBe(ctx.sessionState);
|
|
111
|
+
expect(captured?.workspaceId).toBe('workspace_test');
|
|
112
|
+
expect(captured?.sessionId).toBe('session_test');
|
|
113
|
+
expect(captured?.memoryAccess).toBe('read-only');
|
|
114
|
+
expect(captured?.cwd).toBe('/tmp/workflow-active-worktree');
|
|
115
|
+
expect(captured?.toolCallId).toBe('wf_context');
|
|
116
|
+
expect(captured?.promptCache).toBe(ctx.promptCache);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('streams tool progress and preserves structured result fields', async () => {
|
|
120
|
+
const metadata = {
|
|
121
|
+
kind: 'process_execution' as const,
|
|
122
|
+
shell: 'bash' as const,
|
|
123
|
+
mode: 'foreground' as const,
|
|
124
|
+
commandKind: 'test' as const,
|
|
125
|
+
status: 'completed' as const,
|
|
126
|
+
exitCode: 0,
|
|
127
|
+
};
|
|
128
|
+
const ctx = makeContext(async (_name, _args, _signal, context) => {
|
|
129
|
+
context?.reportProgress?.('running tests');
|
|
130
|
+
await Promise.resolve();
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
content: 'tests passed',
|
|
134
|
+
metadata,
|
|
135
|
+
recovery: 'none',
|
|
136
|
+
sensitivity: 'local',
|
|
137
|
+
visionNote: 'verification image',
|
|
138
|
+
} satisfies ToolResult;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const { events, result } = await collect(
|
|
142
|
+
execToolStep(
|
|
143
|
+
{ id: 'progress', tool: 'Bash', args: { command: 'bun test' } },
|
|
144
|
+
new VarStack(),
|
|
145
|
+
ctx,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(events.map((event) => event.type)).toEqual([
|
|
150
|
+
'tool_start',
|
|
151
|
+
'tool_progress',
|
|
152
|
+
'tool_end',
|
|
153
|
+
]);
|
|
154
|
+
expect(events[1]).toMatchObject({
|
|
155
|
+
type: 'tool_progress',
|
|
156
|
+
toolName: 'Bash',
|
|
157
|
+
toolCallId: 'wf_progress',
|
|
158
|
+
message: 'running tests',
|
|
159
|
+
});
|
|
160
|
+
expect(events[2]).toMatchObject({ type: 'tool_end', metadata });
|
|
161
|
+
expect(result.raw).toMatchObject({
|
|
162
|
+
metadata,
|
|
163
|
+
recovery: 'none',
|
|
164
|
+
sensitivity: 'local',
|
|
165
|
+
visionNote: 'verification image',
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -70,11 +70,11 @@ describe('workflows/webppt.yaml', () => {
|
|
|
70
70
|
expect(String(s.title)).toContain('/11');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// 收尾报告 echo;artifacts 只列最终交付物、不在被 rm 的
|
|
73
|
+
// 收尾报告 echo;artifacts 只列最终交付物、不在被 rm 的 out/webppt/${session}/build 下
|
|
74
74
|
expect(def.steps.find((s) => s.id === 'final_report')?.type).toBe('echo');
|
|
75
75
|
const declared = def.artifacts ?? [];
|
|
76
|
-
expect(declared).toContain('
|
|
77
|
-
for (const a of declared) expect(a.startsWith('
|
|
76
|
+
expect(declared).toContain('out/webppt/${session}/deck.pptx');
|
|
77
|
+
for (const a of declared) expect(a.startsWith('out/webppt/${session}/build')).toBe(false);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
it('需求分析步:style 为空时必问视觉样式(VI 选项 + 自定义输入),AskUserQuestion 可用', async () => {
|
|
@@ -199,7 +199,7 @@ describe('workflows/webppt.yaml', () => {
|
|
|
199
199
|
// 配图分流闸门:image_plan_lint 的 --outline 直接吃 design.json
|
|
200
200
|
const gate = def.steps.find((s) => s.id === 'stage_img_route_gate');
|
|
201
201
|
expect(String(gate?.agent)).toContain('image_plan_lint.py');
|
|
202
|
-
expect(String(gate?.agent)).toContain('--outline
|
|
202
|
+
expect(String(gate?.agent)).toContain('--outline out/webppt/${session}/build/design.json');
|
|
203
203
|
expect(String(gate?.done_when)).toContain('img_route_ok');
|
|
204
204
|
// 顺序:build_loop → img_route_gate → gen_images
|
|
205
205
|
expect(ids.indexOf('stage_img_route_gate')).toBeGreaterThan(ids.indexOf('stage_build_loop'));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ------------------------------------------------------------
|
|
4
4
|
* 锁定 workflows/word.yaml(素材驱动知乎深度长文 Word 工作流)的
|
|
5
5
|
* 「可解析 + 关键骨架」:
|
|
6
|
-
* - name=word,最终产物是
|
|
6
|
+
* - name=word,最终产物是 out/word/${session}/article.docx
|
|
7
7
|
* - inputs 支持 topic(greedy) / source / out / image_intensity / quality / debug_keep_build
|
|
8
8
|
* - Markdown-first:material → outline → article.md → image_map/img_plan → docx skill → OPC
|
|
9
9
|
* - 客户图必须使用;需要解释型视觉时积极文生图,提示词必须结合上下文
|
|
@@ -32,8 +32,8 @@ describe('workflows/word.yaml(素材驱动·知乎深度长文 Word)', () =>
|
|
|
32
32
|
expect(def.description).toContain('知乎深度文章');
|
|
33
33
|
|
|
34
34
|
const declared = def.artifacts ?? [];
|
|
35
|
-
expect(declared).toContain('
|
|
36
|
-
for (const a of declared) expect(a.startsWith('
|
|
35
|
+
expect(declared).toContain('out/word/${session}/article.docx');
|
|
36
|
+
for (const a of declared) expect(a.startsWith('out/word/${session}/build')).toBe(false);
|
|
37
37
|
|
|
38
38
|
const inputNames = (def.inputs ?? []).map((i) => i.name);
|
|
39
39
|
expect(inputNames).toEqual(
|
|
@@ -49,7 +49,7 @@ describe('workflows/word.yaml(素材驱动·知乎深度长文 Word)', () =>
|
|
|
49
49
|
expect(inputNames[0]).toBe('topic');
|
|
50
50
|
expect((def.inputs ?? []).find((i) => i.name === 'topic')?.greedy).toBe(true);
|
|
51
51
|
expect((def.inputs ?? []).find((i) => i.name === 'out')?.default).toBe(
|
|
52
|
-
'
|
|
52
|
+
'out/word/${session}/article.docx',
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
55
|
|
|
@@ -163,11 +163,11 @@ describe('workflows/word.yaml(素材驱动·知乎深度长文 Word)', () =>
|
|
|
163
163
|
const extract = def.steps.find((s) => s.id === 'stage_extract');
|
|
164
164
|
const command = String(extract?.args?.command ?? '');
|
|
165
165
|
|
|
166
|
-
expect(command).toContain('
|
|
167
|
-
expect(command).toContain('rm -rf
|
|
168
|
-
expect(command).toContain('mkdir -p
|
|
169
|
-
expect(command).toContain('
|
|
170
|
-
expect(command).toContain('
|
|
166
|
+
expect(command).toContain('out/word/${session}/build/source.md');
|
|
167
|
+
expect(command).toContain('rm -rf out/word/${session}/build/assets');
|
|
168
|
+
expect(command).toContain('mkdir -p out/word/${session}/build/assets');
|
|
169
|
+
expect(command).toContain('out/word/${session}/build/word_topic_image_gate.json');
|
|
170
|
+
expect(command).toContain('out/word/${session}/build/word_topic_image_gate.md');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
it('生成配图失败时会在导出前硬失败,不会把渐变占位图交付进 Word', async () => {
|