minimal-agent 0.1.9 → 0.2.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/README.md +405 -122
- package/dist/main.js +117 -738
- package/package.json +4 -2
- package/plugins/HOW-TO-WRITE-A-PLUGIN.md +186 -0
- package/plugins/ralph-wiggum/commands/ralph-loop.md +6 -16
- package/plugins/ralph-wiggum/plugin.ts +275 -0
- package/plugins/ralph-wiggum/src/goalState.ts +310 -0
- package/plugins/ralph-wiggum/src/sentinels.ts +24 -0
- package/plugins/ralph-wiggum/src/stopHookRunner.ts +136 -0
- package/plugins/ralph-wiggum/src/verificationGate.ts +252 -0
- package/plugins/ralph-wiggum/test/goalState.test.ts +410 -0
- package/plugins/ralph-wiggum/test/verificationGate.test.ts +122 -0
- package/plugins/workflow-runner/.claude-plugin/plugin.json +5 -0
- package/plugins/workflow-runner/commands/workflow.md +15 -0
- package/plugins/workflow-runner/commands/workflows.md +8 -0
- package/plugins/workflow-runner/plugin.ts +42 -0
- package/plugins/workflow-runner/src/expressions.ts +371 -0
- package/plugins/workflow-runner/src/index.ts +194 -0
- package/plugins/workflow-runner/src/loader.ts +193 -0
- package/plugins/workflow-runner/src/runner.ts +313 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +30 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +54 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +115 -0
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +41 -0
- package/plugins/workflow-runner/src/types.ts +183 -0
- package/plugins/workflow-runner/src/workflowState.ts +65 -0
- package/plugins/workflow-runner/test/cli.e2e.test.ts +114 -0
- package/plugins/workflow-runner/test/e2e.test.ts +268 -0
- package/plugins/workflow-runner/test/expressions.test.ts +140 -0
- package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +27 -0
- package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +49 -0
- package/plugins/workflow-runner/test/graceful.test.ts +139 -0
- package/plugins/workflow-runner/test/loader.test.ts +216 -0
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +230 -0
- package/plugins/workflow-runner/test/runner.test.ts +511 -0
- package/skills/image-gen-openrouter/SKILL.md +121 -0
- package/skills/subtitle-srt/SKILL.md +134 -0
- package/skills/tts-zh/SKILL.md +137 -0
- package/skills/video-compose/SKILL.md +139 -0
- package/workflows/book-review-short.yaml +99 -0
- package/workflows/e2e-write-greet.yaml +27 -0
- package/workflows/schema.json +74 -0
- package/workflows/youtube-shorts.yaml +171 -0
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* test/workflows/pluginRunner.isolation.test.ts
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* V1 红线:默认 T-A-O-R 不退化。无论 workflows/ 是缺失 / 存在 / 全坏,
|
|
6
|
+
* 非 / 输入 100% 走 runQuery,零 workflow 副作用。
|
|
7
|
+
*
|
|
8
|
+
* 断言重点:
|
|
9
|
+
* - 非 / 输入下 runQuery 被调用一次且参数(input + history)原样透传
|
|
10
|
+
* - 非 / 输入下 pluginCache 保持空(说明 discoverPlugins 没被调)
|
|
11
|
+
* - 没有任何 workflow_* / plugin_* 事件
|
|
12
|
+
* - 即使 workflows/ 满是坏 yaml,也不影响以上结论
|
|
13
|
+
*
|
|
14
|
+
* 这是"在不 / 启动的情况下原始 T-A-O-R 要好使"硬约束的回归门。
|
|
15
|
+
* ============================================================
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
19
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.ts';
|
|
24
|
+
import {
|
|
25
|
+
_resetPluginCache,
|
|
26
|
+
discoverPlugins,
|
|
27
|
+
} from '../../../src/plugins/commandRouter.ts';
|
|
28
|
+
import type { LoopEvent, Message, Provider } from '../../../src/types.ts';
|
|
29
|
+
import type { PluginEvent } from '../../../src/plugins/pluginApi.ts';
|
|
30
|
+
|
|
31
|
+
type WfOrLoop = LoopEvent | PluginEvent;
|
|
32
|
+
|
|
33
|
+
const fakeProvider: Provider = {
|
|
34
|
+
name: 'test',
|
|
35
|
+
baseURL: '',
|
|
36
|
+
apiKey: '',
|
|
37
|
+
model: 'm',
|
|
38
|
+
contextWindow: 128000,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface RunQueryCall {
|
|
42
|
+
input: string;
|
|
43
|
+
historyLen: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function collect(
|
|
47
|
+
gen: AsyncGenerator<WfOrLoop, void, void>,
|
|
48
|
+
): Promise<WfOrLoop[]> {
|
|
49
|
+
const out: WfOrLoop[] = [];
|
|
50
|
+
for await (const ev of gen) out.push(ev);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('pluginRunner V1 红线:非 / 输入不被 workflow 污染', () => {
|
|
55
|
+
let tmp: string;
|
|
56
|
+
const calls: RunQueryCall[] = [];
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
tmp = mkdtempSync(join(tmpdir(), 'wf-iso-'));
|
|
60
|
+
process.env.MINIMAL_AGENT_CWD = tmp;
|
|
61
|
+
_resetWorkingDir();
|
|
62
|
+
_resetPluginCache();
|
|
63
|
+
calls.length = 0;
|
|
64
|
+
|
|
65
|
+
mock.module('../../../src/loop.ts', () => ({
|
|
66
|
+
runQuery: async function* (input: string, opts: { history: Message[] }) {
|
|
67
|
+
calls.push({ input, historyLen: opts.history.length });
|
|
68
|
+
yield { type: 'text', delta: 'fake-reply' };
|
|
69
|
+
yield { type: 'turn_done' };
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
delete process.env.MINIMAL_AGENT_CWD;
|
|
76
|
+
_resetWorkingDir();
|
|
77
|
+
_resetPluginCache();
|
|
78
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
async function run(input: string, history: Message[] = []) {
|
|
82
|
+
const { runWithPlugins } = await import('../../../src/plugins/pluginRunner.ts');
|
|
83
|
+
return collect(
|
|
84
|
+
runWithPlugins(input, {
|
|
85
|
+
provider: fakeProvider,
|
|
86
|
+
history,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it('workflows/ 缺失 + 非 / 输入 → 走 runQuery,无 workflow 事件', async () => {
|
|
92
|
+
const events = await run('hello world');
|
|
93
|
+
expect(calls.length).toBe(1);
|
|
94
|
+
expect(calls[0].input).toBe('hello world');
|
|
95
|
+
|
|
96
|
+
const noisyTypes = events
|
|
97
|
+
.map((e) => e.type)
|
|
98
|
+
.filter(
|
|
99
|
+
(t) => t.startsWith('workflow_') || t === 'plugin_progress',
|
|
100
|
+
);
|
|
101
|
+
expect(noisyTypes).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('workflows/ 存在合法 yaml + 非 / 输入 → 仍只走 runQuery(不发现 workflow,不调 listWorkflows)', async () => {
|
|
105
|
+
mkdirSync(join(tmp, 'workflows'), { recursive: true });
|
|
106
|
+
writeFileSync(
|
|
107
|
+
join(tmp, 'workflows', 'pretty.yaml'),
|
|
108
|
+
`name: pretty
|
|
109
|
+
description: x
|
|
110
|
+
steps:
|
|
111
|
+
- id: a
|
|
112
|
+
type: assert
|
|
113
|
+
condition: 'true'
|
|
114
|
+
`,
|
|
115
|
+
'utf8',
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const events = await run('just a chat please');
|
|
119
|
+
expect(calls.length).toBe(1);
|
|
120
|
+
expect(events.some((e) => e.type.startsWith('workflow_'))).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('workflows/ 全是坏 yaml + 非 / 输入 → 仍只走 runQuery', async () => {
|
|
124
|
+
mkdirSync(join(tmp, 'workflows'), { recursive: true });
|
|
125
|
+
writeFileSync(join(tmp, 'workflows', 'broken.yaml'), '@@@\nbad:::\n', 'utf8');
|
|
126
|
+
|
|
127
|
+
const events = await run('hi there');
|
|
128
|
+
expect(calls.length).toBe(1);
|
|
129
|
+
expect(calls[0].input).toBe('hi there');
|
|
130
|
+
expect(events.some((e) => e.type.startsWith('workflow_'))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('非 / 输入 history 长度原样透传给 runQuery', async () => {
|
|
134
|
+
const history: Message[] = [
|
|
135
|
+
{ role: 'system', content: 's' },
|
|
136
|
+
{ role: 'user', content: 'u1' },
|
|
137
|
+
{ role: 'assistant', content: 'a1' },
|
|
138
|
+
];
|
|
139
|
+
await run('next thing', history);
|
|
140
|
+
expect(calls[0].historyLen).toBe(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('非 / 输入下 pluginCache 不被填充(discoverPlugins 未被触发)', async () => {
|
|
144
|
+
await run('plain input no slash');
|
|
145
|
+
// 重置过 cache;非 / 输入应该完全跳过 discoverPlugins
|
|
146
|
+
const plugins = Array.from((await stealCache()).values());
|
|
147
|
+
expect(plugins.length).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('/ 输入下 pluginCache 才被填充(对照组)', async () => {
|
|
151
|
+
await run('/nonexistent-cmd');
|
|
152
|
+
const plugins = Array.from((await stealCache()).values());
|
|
153
|
+
// 走过 discoverPlugins,应该至少能发现 ralph-wiggum(workflow 已降为内置能力)
|
|
154
|
+
expect(plugins.length).toBeGreaterThan(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* commandRouter 的 pluginCache 是模块私有变量,没有 getter。
|
|
160
|
+
* 用一个 hack:调用 discoverPlugins(),但只看返回 —— 如果之前没被填,
|
|
161
|
+
* 这一调用结束时会把所有插件加进 cache。所以我们改为读副作用更直接的接口:
|
|
162
|
+
* listAvailableCommands()。如果没 discoverPlugins 过,listAvailableCommands
|
|
163
|
+
* 会返回空数组。
|
|
164
|
+
*/
|
|
165
|
+
async function stealCache(): Promise<Map<string, unknown>> {
|
|
166
|
+
const { listAvailableCommands } = await import('../../../src/plugins/commandRouter.ts');
|
|
167
|
+
const cmds = listAvailableCommands();
|
|
168
|
+
const m = new Map<string, unknown>();
|
|
169
|
+
for (const c of cmds) m.set(c.plugin, c);
|
|
170
|
+
return m;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
describe('pluginRunner V2 红线:/workflow 命令通过 plugin.ts.runCommand 接管', () => {
|
|
174
|
+
let tmp: string;
|
|
175
|
+
const calls: RunQueryCall[] = [];
|
|
176
|
+
|
|
177
|
+
beforeEach(() => {
|
|
178
|
+
tmp = mkdtempSync(join(tmpdir(), 'wf-iso2-'));
|
|
179
|
+
process.env.MINIMAL_AGENT_CWD = tmp;
|
|
180
|
+
_resetWorkingDir();
|
|
181
|
+
_resetPluginCache();
|
|
182
|
+
calls.length = 0;
|
|
183
|
+
|
|
184
|
+
mock.module('../../../src/loop.ts', () => ({
|
|
185
|
+
runQuery: async function* (input: string, opts: { history: Message[] }) {
|
|
186
|
+
calls.push({ input, historyLen: opts.history.length });
|
|
187
|
+
yield { type: 'text', delta: 'should-not-be-here' };
|
|
188
|
+
yield { type: 'turn_done' };
|
|
189
|
+
},
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
delete process.env.MINIMAL_AGENT_CWD;
|
|
195
|
+
_resetWorkingDir();
|
|
196
|
+
_resetPluginCache();
|
|
197
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('/workflows 由插件 plugin.ts 接管 → 不走 runQuery,yield 列表 text', async () => {
|
|
201
|
+
const { runWithPlugins } = await import('../../../src/plugins/pluginRunner.ts');
|
|
202
|
+
const events = await collect(
|
|
203
|
+
runWithPlugins('/workflows', { provider: fakeProvider, history: [] }),
|
|
204
|
+
);
|
|
205
|
+
expect(calls.length).toBe(0);
|
|
206
|
+
expect(events.some((e) => e.type === 'text')).toBe(true);
|
|
207
|
+
expect(events.some((e) => e.type === 'turn_done')).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('/workflow book-review-short 缺必填 input → yield error,不走 runQuery', async () => {
|
|
211
|
+
const { runWithPlugins } = await import('../../../src/plugins/pluginRunner.ts');
|
|
212
|
+
const events = await collect(
|
|
213
|
+
runWithPlugins('/workflow book-review-short', {
|
|
214
|
+
provider: fakeProvider,
|
|
215
|
+
history: [],
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
expect(calls.length).toBe(0);
|
|
219
|
+
expect(events.some((e) => e.type === 'error')).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('/workflow 以插件命令形式出现在 listAvailableCommands', async () => {
|
|
223
|
+
await discoverPlugins();
|
|
224
|
+
const { listAvailableCommands } = await import('../../../src/plugins/commandRouter.ts');
|
|
225
|
+
const cmds = listAvailableCommands();
|
|
226
|
+
const wf = cmds.find((c) => c.command === '/workflow');
|
|
227
|
+
expect(wf).toBeDefined();
|
|
228
|
+
expect(wf?.plugin).toBe('workflow-runner');
|
|
229
|
+
});
|
|
230
|
+
});
|