minimal-agent 0.2.0 → 0.3.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.
Files changed (107) hide show
  1. package/README.md +50 -72
  2. package/package.json +18 -13
  3. package/plugins/ralph-wiggum/plugin.js +205 -0
  4. package/plugins/ralph-wiggum/src/goalState.js +260 -0
  5. package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
  6. package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
  7. package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
  8. package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
  9. package/plugins/workflow-runner/src/expressions.js +369 -0
  10. package/plugins/workflow-runner/src/index.js +174 -0
  11. package/plugins/workflow-runner/src/loader.js +183 -0
  12. package/plugins/workflow-runner/src/runner.js +290 -0
  13. package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
  14. package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
  15. package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
  16. package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
  17. package/plugins/workflow-runner/src/types.js +59 -0
  18. package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
  19. package/src/bootstrap/cwdArg.js +22 -0
  20. package/src/bootstrap/workingDir.js +31 -0
  21. package/src/cli/configWizard.js +272 -0
  22. package/src/cli/print.js +192 -0
  23. package/src/config/configFile.js +78 -0
  24. package/src/config.js +118 -0
  25. package/src/context/compact.js +357 -0
  26. package/src/context/microCompactLite.js +151 -0
  27. package/src/context/persistContext.js +109 -0
  28. package/src/context/reactiveCompact.js +121 -0
  29. package/src/context/sessionPath.js +58 -0
  30. package/src/context/snipCompact.js +112 -0
  31. package/src/context/tokenCounter.js +66 -0
  32. package/src/llm/client.js +182 -0
  33. package/src/loop.js +230 -0
  34. package/src/main.js +116 -0
  35. package/src/plugin-sdk.js +24 -0
  36. package/src/plugins/commandRouter.js +169 -0
  37. package/src/plugins/hookEngine.js +258 -0
  38. package/src/plugins/pluginApi.js +23 -0
  39. package/src/plugins/pluginLoader.js +71 -0
  40. package/src/plugins/pluginRunner.js +65 -0
  41. package/src/plugins/transcript.js +171 -0
  42. package/src/prompts/projectInstructions.js +48 -0
  43. package/src/prompts/skillList.js +126 -0
  44. package/src/prompts/system.js +155 -0
  45. package/src/session/runTurn.js +41 -0
  46. package/src/session/sessionState.js +19 -0
  47. package/src/tools/bash/bash.js +352 -0
  48. package/src/tools/bash/semantics.js +85 -0
  49. package/src/tools/bash/warnings.js +98 -0
  50. package/src/tools/edit/edit.js +253 -0
  51. package/src/tools/edit/multi-edit.js +155 -0
  52. package/src/tools/glob/glob.js +97 -0
  53. package/src/tools/grep/grep.js +185 -0
  54. package/src/tools/grep/rgPath.js +173 -0
  55. package/src/tools/index.js +94 -0
  56. package/src/tools/read/read.js +209 -0
  57. package/src/tools/shared/fileState.js +61 -0
  58. package/src/tools/shared/fileUtils.js +281 -0
  59. package/src/tools/shared/schemas.js +16 -0
  60. package/src/tools/types.js +21 -0
  61. package/src/tools/webbrowser/browser.js +55 -0
  62. package/src/tools/webbrowser/webbrowser.js +194 -0
  63. package/src/tools/webfetch/preapproved.js +267 -0
  64. package/src/tools/webfetch/webfetch.js +317 -0
  65. package/src/tools/websearch/websearch.js +161 -0
  66. package/src/tools/write/write.js +125 -0
  67. package/src/types/turndown.d.ts +23 -0
  68. package/src/types.js +16 -0
  69. package/src/ui/App.js +37 -0
  70. package/src/ui/InputBox.js +240 -0
  71. package/src/ui/MessageList.js +28 -0
  72. package/src/ui/Root.js +70 -0
  73. package/src/ui/StatusLine.js +41 -0
  74. package/src/ui/ToolStatus.js +11 -0
  75. package/src/ui/hooks/useChat.js +234 -0
  76. package/src/ui/hooks/usePasteHandler.js +137 -0
  77. package/src/ui/hooks/useTextBuffer.js +55 -0
  78. package/src/ui/hooks/useTokenUsage.js +30 -0
  79. package/src/ui/textBuffer.js +217 -0
  80. package/src/utils/packageRoot.js +37 -0
  81. package/src/utils/resourcePaths.js +49 -0
  82. package/src/utils/zodToJson.js +29 -0
  83. package/dist/main.js +0 -5315
  84. package/plugins/ralph-wiggum/plugin.ts +0 -275
  85. package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
  86. package/plugins/ralph-wiggum/src/goalState.ts +0 -310
  87. package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
  88. package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
  89. package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
  90. package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
  91. package/plugins/workflow-runner/src/expressions.ts +0 -371
  92. package/plugins/workflow-runner/src/index.ts +0 -194
  93. package/plugins/workflow-runner/src/loader.ts +0 -193
  94. package/plugins/workflow-runner/src/runner.ts +0 -313
  95. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
  96. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
  97. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
  98. package/plugins/workflow-runner/src/types.ts +0 -183
  99. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
  100. package/plugins/workflow-runner/test/e2e.test.ts +0 -268
  101. package/plugins/workflow-runner/test/expressions.test.ts +0 -140
  102. package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
  103. package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
  104. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  105. package/plugins/workflow-runner/test/loader.test.ts +0 -216
  106. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
  107. package/plugins/workflow-runner/test/runner.test.ts +0 -511
@@ -1,140 +0,0 @@
1
- /**
2
- * ============================================================
3
- * test/workflows/expressions.test.ts
4
- * ------------------------------------------------------------
5
- * workflow 表达式引擎单测:tokenizer / evalExpr / interpolate /
6
- * interpolateDeep。重点覆盖:
7
- * - 变量点路径(inputs.book)
8
- * - 比较 + 布尔运算
9
- * - 白名单内置函数(fileExists / length / lower / upper)
10
- * - ${} 插值(包括括号匹配 + 转义 + 对象 JSON 化)
11
- * - 安全:禁止 eval/Function 等被插入
12
- * ============================================================
13
- */
14
-
15
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
16
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
17
- import { tmpdir } from 'node:os';
18
- import { join } from 'node:path';
19
-
20
- import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.ts';
21
- import {
22
- evalExpr,
23
- interpolate,
24
- interpolateDeep,
25
- } from '../src/expressions.ts';
26
- import { VarStack } from '../src/types.ts';
27
-
28
- describe('workflows/expressions', () => {
29
- let vars: VarStack;
30
-
31
- beforeEach(() => {
32
- vars = new VarStack();
33
- vars.setGlobal('inputs', { book: '原则', angle: 'insight', n: 3 });
34
- vars.set('thumb_prompt', 'a book and a lamp');
35
- });
36
-
37
- it('字面量数字 / 字符串 / 布尔', () => {
38
- expect(evalExpr('42', vars)).toBe(42);
39
- expect(evalExpr('"hello"', vars)).toBe('hello');
40
- expect(evalExpr("'x'", vars)).toBe('x');
41
- expect(evalExpr('true', vars)).toBe(true);
42
- expect(evalExpr('false', vars)).toBe(false);
43
- expect(evalExpr('null', vars)).toBe(null);
44
- });
45
-
46
- it('变量 + 点路径', () => {
47
- expect(evalExpr('thumb_prompt', vars)).toBe('a book and a lamp');
48
- expect(evalExpr('inputs.book', vars)).toBe('原则');
49
- expect(evalExpr('inputs.n', vars)).toBe(3);
50
- expect(evalExpr('inputs.missing', vars)).toBeUndefined();
51
- });
52
-
53
- it('比较运算 ==/!=/>/<', () => {
54
- expect(evalExpr('inputs.n == 3', vars)).toBe(true);
55
- expect(evalExpr('inputs.n != 4', vars)).toBe(true);
56
- expect(evalExpr('inputs.n > 2', vars)).toBe(true);
57
- expect(evalExpr('inputs.n < 2', vars)).toBe(false);
58
- expect(evalExpr('"a" == "a"', vars)).toBe(true);
59
- });
60
-
61
- it('布尔运算 && / || / !', () => {
62
- expect(evalExpr('true && false', vars)).toBe(false);
63
- expect(evalExpr('true || false', vars)).toBe(true);
64
- expect(evalExpr('!false', vars)).toBe(true);
65
- expect(evalExpr('inputs.n > 0 && inputs.book == "原则"', vars)).toBe(true);
66
- });
67
-
68
- it('白名单函数 length / lower / upper', () => {
69
- expect(evalExpr('length("abcd")', vars)).toBe(4);
70
- expect(evalExpr('length(inputs.book)', vars)).toBe(2);
71
- expect(evalExpr('lower("HELLO")', vars)).toBe('hello');
72
- expect(evalExpr('upper("hi")', vars)).toBe('HI');
73
- });
74
-
75
- it('未知函数被拒绝(白名单外)', () => {
76
- expect(() => evalExpr('eval("1+1")', vars)).toThrow();
77
- expect(() => evalExpr('require("fs")', vars)).toThrow();
78
- expect(() => evalExpr('Function("x")', vars)).toThrow();
79
- });
80
-
81
- it('interpolate ${EXPR} 替换变量', () => {
82
- expect(interpolate('book=${inputs.book}', vars)).toBe('book=原则');
83
- expect(interpolate('${inputs.n} times', vars)).toBe('3 times');
84
- });
85
-
86
- it('interpolate 处理对象 → JSON.stringify', () => {
87
- const out = interpolate('${inputs}', vars);
88
- expect(out).toContain('原则');
89
- expect(out).toContain('insight');
90
- });
91
-
92
- it('interpolate 支持 \\$ 转义', () => {
93
- expect(interpolate('price=\\${inputs.n}', vars)).toBe('price=${inputs.n}');
94
- });
95
-
96
- it('interpolate 未闭合 ${ 原样保留', () => {
97
- expect(interpolate('hello ${unclosed', vars)).toBe('hello ${unclosed');
98
- });
99
-
100
- it('interpolateDeep 递归处理对象/数组', () => {
101
- const input = {
102
- file_path: 'videos/${inputs.book}/script.txt',
103
- content: '${thumb_prompt}',
104
- nested: { list: ['${inputs.angle}', 'static'] },
105
- };
106
- const out = interpolateDeep(input, vars);
107
- expect(out.file_path).toBe('videos/原则/script.txt');
108
- expect(out.content).toBe('a book and a lamp');
109
- expect(out.nested.list[0]).toBe('insight');
110
- expect(out.nested.list[1]).toBe('static');
111
- });
112
- });
113
-
114
- describe('workflows/expressions::fileExists', () => {
115
- let tmp: string;
116
-
117
- beforeEach(() => {
118
- tmp = mkdtempSync(join(tmpdir(), 'wf-expr-'));
119
- process.env.MINIMAL_AGENT_CWD = tmp;
120
- _resetWorkingDir();
121
- mock.module('../../../src/bootstrap/workingDir.ts', () => ({
122
- getWorkingDir: () => tmp,
123
- initWorkingDir: () => tmp,
124
- _resetWorkingDir: () => {},
125
- }));
126
- });
127
-
128
- afterEach(() => {
129
- delete process.env.MINIMAL_AGENT_CWD;
130
- _resetWorkingDir();
131
- rmSync(tmp, { recursive: true, force: true });
132
- });
133
-
134
- it('fileExists 命中真实文件', () => {
135
- writeFileSync(join(tmp, 'a.txt'), 'x');
136
- const vars = new VarStack();
137
- expect(evalExpr('fileExists("a.txt")', vars)).toBe(true);
138
- expect(evalExpr('fileExists("missing.txt")', vars)).toBe(false);
139
- });
140
- });
@@ -1,27 +0,0 @@
1
- name: cli-e2e
2
- description: CLI 端到端测试 fixture:纯 Write + assert,不调 LLM 不调网络
3
- version: "0.1"
4
-
5
- inputs:
6
- - name: topic
7
- type: string
8
- required: true
9
-
10
- steps:
11
- - id: write_note
12
- tool: Write
13
- args:
14
- file_path: "out/${inputs.topic}/note.txt"
15
- content: "topic=${inputs.topic} ok"
16
- capture:
17
- args.content: note_body
18
-
19
- - id: verify_file
20
- type: assert
21
- condition: 'fileExists("out/${inputs.topic}/note.txt")'
22
- onFail: "note.txt 未落盘"
23
-
24
- - id: verify_capture
25
- type: assert
26
- condition: 'length(note_body) > 0'
27
- onFail: "capture 没绑到 note_body"
@@ -1,49 +0,0 @@
1
- name: hello-workflow
2
- description: |
3
- E2E 测试用极简 workflow:不依赖 LLM、不调网络,只用确定性工具。
4
- 覆盖:input 必填校验 / ${var} 插值 / 嵌套目录 / 多步 capture / assert+fileExists。
5
- version: "0.1"
6
-
7
- inputs:
8
- - name: name
9
- type: string
10
- required: true
11
- description: 收件人名字
12
- - name: lang
13
- type: enum
14
- values: [zh, en]
15
- default: en
16
-
17
- steps:
18
- # 1. 写问候语文件 + capture 内容
19
- - id: write_greeting
20
- tool: Write
21
- args:
22
- file_path: "greetings/${inputs.name}/hello.txt"
23
- content: "Hello, ${inputs.name}! (lang=${inputs.lang})"
24
- capture:
25
- args.content: greeting_text
26
-
27
- # 2. 写 meta json
28
- - id: write_meta
29
- tool: Write
30
- args:
31
- file_path: "greetings/${inputs.name}/meta.json"
32
- content: |
33
- {
34
- "name": "${inputs.name}",
35
- "lang": "${inputs.lang}",
36
- "greeting_length": ${length(greeting_text)}
37
- }
38
-
39
- # 3. 守门 assert:两个文件都得在
40
- - id: verify_files
41
- type: assert
42
- condition: 'fileExists("greetings/${inputs.name}/hello.txt") && fileExists("greetings/${inputs.name}/meta.json")'
43
- onFail: "hello-workflow 文件未全部落盘"
44
-
45
- # 4. 守门 assert:capture 的变量非空
46
- - id: verify_capture
47
- type: assert
48
- condition: 'length(greeting_text) > 0'
49
- onFail: "capture 没绑到 greeting_text"
@@ -1,139 +0,0 @@
1
- /**
2
- * ============================================================
3
- * test/workflows/graceful.test.ts
4
- * ------------------------------------------------------------
5
- * 降级契约(R1):
6
- * - workflows/ 不存在 → listWorkflows() 返回包内自带 [包内 yaml],绝不抛错
7
- * - 全是坏 yaml → 跳过坏的,列出好的
8
- * - findWorkflowByName 未命中 → 返回 null(让调用方 yield 友好 error)
9
- * - runWorkflowFromCommand 对未知 name → yield error,不污染 history
10
- * - runWorkflowsList 在空列表时 yield 友好提示
11
- * ============================================================
12
- */
13
-
14
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
15
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
16
- import { tmpdir } from 'node:os';
17
- import { join } from 'node:path';
18
-
19
- import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.ts';
20
- import { listWorkflows } from '../src/loader.ts';
21
- import {
22
- runWorkflowFromCommand,
23
- runWorkflowsList,
24
- } from '../src/index.ts';
25
- import type { LoopEvent, Provider } from '../../../src/types.ts';
26
- import type { WorkflowEvent } from '../src/types.ts';
27
-
28
- type WfOrLoop = WorkflowEvent | LoopEvent;
29
-
30
- const fakeProvider: Provider = {
31
- name: 'test',
32
- baseURL: '',
33
- apiKey: '',
34
- model: 'm',
35
- contextWindow: 128000,
36
- };
37
-
38
- async function collect(
39
- gen: AsyncGenerator<WfOrLoop, void, void>,
40
- ): Promise<WfOrLoop[]> {
41
- const out: WfOrLoop[] = [];
42
- for await (const ev of gen) out.push(ev);
43
- return out;
44
- }
45
-
46
- describe('workflows/graceful', () => {
47
- let tmp: string;
48
-
49
- beforeEach(() => {
50
- tmp = mkdtempSync(join(tmpdir(), 'wf-graceful-'));
51
- process.env.MINIMAL_AGENT_CWD = tmp;
52
- _resetWorkingDir();
53
- mock.module('../../../src/bootstrap/workingDir.ts', () => ({
54
- getWorkingDir: () => tmp,
55
- initWorkingDir: () => tmp,
56
- _resetWorkingDir: () => {},
57
- }));
58
- });
59
-
60
- afterEach(() => {
61
- delete process.env.MINIMAL_AGENT_CWD;
62
- _resetWorkingDir();
63
- rmSync(tmp, { recursive: true, force: true });
64
- });
65
-
66
- it('cwd 下无 workflows/ 目录 → listWorkflows() 仍返回(packageRoot fallback 也只是空降级,不抛)', async () => {
67
- const list = await listWorkflows();
68
- // 至少应该包含包内自带的 book-review-short
69
- expect(Array.isArray(list)).toBe(true);
70
- });
71
-
72
- it('cwd 下 workflows/ 全空 → 不抛,不增加任何条目', async () => {
73
- mkdirSync(join(tmp, 'workflows'), { recursive: true });
74
- const list = await listWorkflows();
75
- expect(Array.isArray(list)).toBe(true);
76
- });
77
-
78
- it('cwd 下全是坏 yaml → 不抛,只跳过坏的', async () => {
79
- mkdirSync(join(tmp, 'workflows'), { recursive: true });
80
- writeYamlIn(tmp, 'bad1.yaml', 'name: x\n : weird:::\n');
81
- writeYamlIn(tmp, 'bad2.yaml', '@@@@@@\n');
82
- const list = await listWorkflows();
83
- // 不抛错,至少包内自带的仍然可见
84
- expect(Array.isArray(list)).toBe(true);
85
- });
86
-
87
- it('/workflow unknown-name → yield error,无 workflow_start', async () => {
88
- const events = await collect(
89
- runWorkflowFromCommand('definitely-not-a-real-wf-xyz', {
90
- provider: fakeProvider,
91
- history: [],
92
- }),
93
- );
94
- expect(events.some((e) => e.type === 'error')).toBe(true);
95
- expect(events.some((e) => e.type === 'workflow_start')).toBe(false);
96
- });
97
-
98
- it('/workflow(不带 name)→ yield 用法提示 error', async () => {
99
- const events = await collect(
100
- runWorkflowFromCommand('', {
101
- provider: fakeProvider,
102
- history: [],
103
- }),
104
- );
105
- const err = events.find(
106
- (e): e is Extract<LoopEvent, { type: 'error' }> => e.type === 'error',
107
- );
108
- expect(err).toBeDefined();
109
- expect(err!.error).toContain('/workflow');
110
- });
111
-
112
- it('/workflows 列表 yield text 事件 + turn_done', async () => {
113
- const events = await collect(runWorkflowsList());
114
- expect(events.some((e) => e.type === 'text')).toBe(true);
115
- expect(events[events.length - 1].type).toBe('turn_done');
116
- });
117
-
118
- it('缺必填 input → yield error,不进入 runWorkflow', async () => {
119
- // book-review-short 要求 book 必填
120
- const events = await collect(
121
- runWorkflowFromCommand('book-review-short', {
122
- provider: fakeProvider,
123
- history: [],
124
- }),
125
- );
126
- const err = events.find(
127
- (e): e is Extract<LoopEvent, { type: 'error' }> => e.type === 'error',
128
- );
129
- expect(err).toBeDefined();
130
- expect(err!.error).toContain('book');
131
- // 没有 workflow_start
132
- expect(events.some((e) => e.type === 'workflow_start')).toBe(false);
133
- });
134
- });
135
-
136
- function writeYamlIn(root: string, name: string, body: string) {
137
- mkdirSync(join(root, 'workflows'), { recursive: true });
138
- writeFileSync(join(root, 'workflows', name), body, 'utf8');
139
- }
@@ -1,216 +0,0 @@
1
- /**
2
- * ============================================================
3
- * test/workflows/loader.test.ts
4
- * ------------------------------------------------------------
5
- * loader 单测:
6
- * - 双源扫描:cwd 优先、packageRoot fallback
7
- * - 一个坏 yaml 不污染其它工作流
8
- * - 校验阻断:name/description/steps/动作字段
9
- * - 内置 book-review-short.yaml 能被解析
10
- * ============================================================
11
- */
12
-
13
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
14
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
15
- import { tmpdir } from 'node:os';
16
- import { join } from 'node:path';
17
-
18
- import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.ts';
19
- import {
20
- findWorkflowByName,
21
- listWorkflows,
22
- parseWorkflowFile,
23
- WorkflowLoadError,
24
- } from '../src/loader.ts';
25
-
26
- describe('workflows/loader', () => {
27
- let tmp: string;
28
-
29
- beforeEach(() => {
30
- tmp = mkdtempSync(join(tmpdir(), 'wf-loader-'));
31
- process.env.MINIMAL_AGENT_CWD = tmp;
32
- _resetWorkingDir();
33
- // 抵抗其它测试文件(如 pluginRunner.test.ts)通过 mock.module 留下的
34
- // workingDir.ts 全局替身——把 getWorkingDir 重新指回本测试的 tmp。
35
- mock.module('../../../src/bootstrap/workingDir.ts', () => ({
36
- getWorkingDir: () => tmp,
37
- initWorkingDir: () => tmp,
38
- _resetWorkingDir: () => {},
39
- }));
40
- });
41
-
42
- afterEach(() => {
43
- delete process.env.MINIMAL_AGENT_CWD;
44
- _resetWorkingDir();
45
- rmSync(tmp, { recursive: true, force: true });
46
- });
47
-
48
- function writeYaml(name: string, body: string) {
49
- mkdirSync(join(tmp, 'workflows'), { recursive: true });
50
- writeFileSync(join(tmp, 'workflows', name), body, 'utf8');
51
- }
52
-
53
- it('cwd 下放 yaml 能被 listWorkflows 发现', async () => {
54
- writeYaml(
55
- 'my.yaml',
56
- `name: my-wf
57
- description: hello
58
- steps:
59
- - id: a
60
- tool: Read
61
- args: { file_path: a.txt }
62
- `,
63
- );
64
-
65
- const list = await listWorkflows();
66
- expect(list.find((w) => w.name === 'my-wf')).toBeDefined();
67
- });
68
-
69
- it('packageRoot 自带 book-review-short 也能被发现(fallback 路径)', async () => {
70
- const list = await listWorkflows();
71
- expect(list.find((w) => w.name === 'book-review-short')).toBeDefined();
72
- });
73
-
74
- it('cwd 同名 workflow 覆盖 packageRoot', async () => {
75
- writeYaml(
76
- 'override.yaml',
77
- `name: book-review-short
78
- description: cwd override
79
- steps:
80
- - id: only
81
- type: assert
82
- condition: 'true'
83
- `,
84
- );
85
- const def = await findWorkflowByName('book-review-short');
86
- expect(def).not.toBeNull();
87
- expect(def!.description).toBe('cwd override');
88
- });
89
-
90
- it('坏 yaml 不污染其他 workflow(继续列出好的)', async () => {
91
- writeYaml('good.yaml', `name: good
92
- description: ok
93
- steps:
94
- - id: x
95
- type: assert
96
- condition: 'true'
97
- `);
98
- writeYaml('bad.yaml', 'name: bad\n bad-indent\n: garbage:::\n');
99
-
100
- const list = await listWorkflows();
101
- expect(list.find((w) => w.name === 'good')).toBeDefined();
102
- // bad.yaml 不应阻塞其它,也不应被列出
103
- expect(list.find((w) => w.name === 'bad')).toBeUndefined();
104
- });
105
-
106
- it('findWorkflowByName 未命中返回 null', async () => {
107
- const def = await findWorkflowByName('nonexistent-wf-xyz');
108
- expect(def).toBeNull();
109
- });
110
-
111
- it('listWorkflows 对完全缺失 workflows/ 不抛错', async () => {
112
- // 没有 workflows 目录,只有 packageRoot fallback
113
- const list = await listWorkflows();
114
- expect(Array.isArray(list)).toBe(true);
115
- });
116
- });
117
-
118
- describe('workflows/loader::validate', () => {
119
- let tmp: string;
120
-
121
- beforeEach(() => {
122
- tmp = mkdtempSync(join(tmpdir(), 'wf-loader-v-'));
123
- });
124
-
125
- afterEach(() => {
126
- rmSync(tmp, { recursive: true, force: true });
127
- });
128
-
129
- function writeRaw(body: string): string {
130
- const p = join(tmp, 'wf.yaml');
131
- writeFileSync(p, body, 'utf8');
132
- return p;
133
- }
134
-
135
- it('缺 name → 抛 WorkflowLoadError', async () => {
136
- const p = writeRaw('description: x\nsteps:\n - id: a\n type: assert\n condition: "true"\n');
137
- await expect(parseWorkflowFile(p)).rejects.toThrow(WorkflowLoadError);
138
- });
139
-
140
- it('缺 description → 抛错', async () => {
141
- const p = writeRaw('name: x\nsteps:\n - id: a\n type: assert\n condition: "true"\n');
142
- await expect(parseWorkflowFile(p)).rejects.toThrow();
143
- });
144
-
145
- it('steps 空数组 → 抛错', async () => {
146
- const p = writeRaw('name: x\ndescription: y\nsteps: []\n');
147
- await expect(parseWorkflowFile(p)).rejects.toThrow();
148
- });
149
-
150
- it('step 没有任何动作字段 → 抛错', async () => {
151
- const p = writeRaw('name: x\ndescription: y\nsteps:\n - id: a\n');
152
- await expect(parseWorkflowFile(p)).rejects.toThrow();
153
- });
154
-
155
- it('step 同时设置 tool + llm → 抛错', async () => {
156
- const p = writeRaw(`name: x
157
- description: y
158
- steps:
159
- - id: a
160
- tool: Read
161
- llm: "say hi"
162
- `);
163
- await expect(parseWorkflowFile(p)).rejects.toThrow();
164
- });
165
-
166
- it('assert step 缺 condition → 抛错', async () => {
167
- const p = writeRaw(`name: x
168
- description: y
169
- steps:
170
- - id: a
171
- type: assert
172
- `);
173
- await expect(parseWorkflowFile(p)).rejects.toThrow();
174
- });
175
-
176
- it('同级 steps id 重复 → 抛错', async () => {
177
- const p = writeRaw(`name: x
178
- description: y
179
- steps:
180
- - id: dup
181
- type: assert
182
- condition: 'true'
183
- - id: dup
184
- type: assert
185
- condition: 'true'
186
- `);
187
- await expect(parseWorkflowFile(p)).rejects.toThrow();
188
- });
189
-
190
- it('合法 yaml 通过 + __source 字段被注入', async () => {
191
- const p = writeRaw(`name: ok-wf
192
- description: nice
193
- steps:
194
- - id: a
195
- tool: Read
196
- args: { file_path: x.txt }
197
- `);
198
- const def = await parseWorkflowFile(p);
199
- expect(def.name).toBe('ok-wf');
200
- expect(def.__source).toBe(p);
201
- });
202
-
203
- it('inputs[].type=enum 缺 values → 抛错', async () => {
204
- const p = writeRaw(`name: x
205
- description: y
206
- inputs:
207
- - name: foo
208
- type: enum
209
- steps:
210
- - id: a
211
- type: assert
212
- condition: 'true'
213
- `);
214
- await expect(parseWorkflowFile(p)).rejects.toThrow();
215
- });
216
- });