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,410 +0,0 @@
1
- import { describe, expect, it } from 'bun:test';
2
- import { mkdirSync, rmSync, existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { GoalState, Phase } from '../src/goalState.ts';
6
-
7
- function createTempDir(): string {
8
- const dir = join(tmpdir(), `goalstate-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
9
- mkdirSync(dir, { recursive: true });
10
- return dir;
11
- }
12
-
13
- function cleanup(dir: string): void {
14
- try {
15
- rmSync(dir, { recursive: true });
16
- } catch {
17
- }
18
- }
19
-
20
- describe('goalState', () => {
21
- describe('init', () => {
22
- it('创建所有状态文件', async () => {
23
- const tmpDir = createTempDir();
24
- try {
25
- const gs = new GoalState(tmpDir);
26
- await gs.init('Build a REST API', []);
27
-
28
- expect(existsSync(join(tmpDir, '.minimal-agent', 'goal.md'))).toBe(true);
29
- expect(existsSync(join(tmpDir, '.minimal-agent', 'completion.md'))).toBe(true);
30
- expect(existsSync(join(tmpDir, '.minimal-agent', 'phase.md'))).toBe(true);
31
- expect(existsSync(join(tmpDir, '.minimal-agent', 'progress.md'))).toBe(true);
32
- expect(existsSync(join(tmpDir, '.minimal-agent', 'learnings.md'))).toBe(true);
33
- expect(existsSync(join(tmpDir, '.minimal-agent', 'decisions.md'))).toBe(true);
34
- } finally {
35
- cleanup(tmpDir);
36
- }
37
- });
38
-
39
- it('goal.md 包含目标文本', async () => {
40
- const tmpDir = createTempDir();
41
- try {
42
- const gs = new GoalState(tmpDir);
43
- await gs.init('Fix auth bug in login module', []);
44
- expect(gs.goal).toContain('Fix auth bug');
45
- } finally {
46
- cleanup(tmpDir);
47
- }
48
- });
49
-
50
- it('初始阶段为 PLAN', async () => {
51
- const tmpDir = createTempDir();
52
- try {
53
- const gs = new GoalState(tmpDir);
54
- await gs.init('Test goal', []);
55
- expect(gs.currentPhase).toBe(Phase.PLAN);
56
- } finally {
57
- cleanup(tmpDir);
58
- }
59
- });
60
-
61
- it('重复 init 不覆盖已有文件', async () => {
62
- const tmpDir = createTempDir();
63
- try {
64
- const gs = new GoalState(tmpDir);
65
- await gs.init('First goal', []);
66
- await gs.init('Second goal', []);
67
- expect(gs.goal).toContain('First goal');
68
- } finally {
69
- cleanup(tmpDir);
70
- }
71
- });
72
- });
73
-
74
- describe('phase management', () => {
75
- it('PLAN → BUILD 合法切换', async () => {
76
- const tmpDir = createTempDir();
77
- try {
78
- const gs = new GoalState(tmpDir);
79
- await gs.init('Test', []);
80
- await gs.setPhase(Phase.BUILD, 'plan_complete');
81
- expect(gs.currentPhase).toBe(Phase.BUILD);
82
- } finally {
83
- cleanup(tmpDir);
84
- }
85
- });
86
-
87
- it('setPhase 的 reason 是日志文本,不参与 FSM 校验', async () => {
88
- const tmpDir = createTempDir();
89
- try {
90
- const gs = new GoalState(tmpDir);
91
- await gs.init('Test', []);
92
- // 用任意中文 reason,合法目标阶段仍应成功
93
- await gs.setPhase(Phase.BUILD, '随便写的日志文本,不该被当成事件名');
94
- expect(gs.currentPhase).toBe(Phase.BUILD);
95
- } finally {
96
- cleanup(tmpDir);
97
- }
98
- });
99
-
100
- it('非法阶段切换抛出错误(基于目标阶段,与 reason 无关)', async () => {
101
- const tmpDir = createTempDir();
102
- try {
103
- const gs = new GoalState(tmpDir);
104
- await gs.init('Test', []);
105
- // PLAN → VERIFY 在 PHASE_TRANSITIONS[PLAN] 的 values 中不存在
106
- await expect(
107
- gs.setPhase(Phase.VERIFY, '哪怕写一个看似合法的英文 event 名也不行'),
108
- ).rejects.toThrow();
109
- } finally {
110
- cleanup(tmpDir);
111
- }
112
- });
113
-
114
- it('forceSetPhase 允许任意切换', async () => {
115
- const tmpDir = createTempDir();
116
- try {
117
- const gs = new GoalState(tmpDir);
118
- await gs.init('Test', []);
119
- await gs.forceSetPhase(Phase.HEAL, 'manual override');
120
- expect(gs.currentPhase).toBe(Phase.HEAL);
121
- } finally {
122
- cleanup(tmpDir);
123
- }
124
- });
125
- });
126
-
127
- describe('progress log', () => {
128
- it('追加进度日志', async () => {
129
- const tmpDir = createTempDir();
130
- try {
131
- const gs = new GoalState(tmpDir);
132
- await gs.init('Test', []);
133
- await gs.appendProgress('Iteration 1: started');
134
- await gs.appendProgress('Iteration 2: done');
135
-
136
- const tail = gs.tailProgress(10);
137
- expect(tail).toContain('Iteration 1: started');
138
- expect(tail).toContain('Iteration 2: done');
139
- } finally {
140
- cleanup(tmpDir);
141
- }
142
- });
143
-
144
- it('tailProgress 只返回最近 N 行', async () => {
145
- const tmpDir = createTempDir();
146
- try {
147
- const gs = new GoalState(tmpDir);
148
- await gs.init('Test', []);
149
- for (let i = 0; i < 20; i++) {
150
- await gs.appendProgress(`Line ${i}`);
151
- }
152
-
153
- const tail = gs.tailProgress(3);
154
- const lines = tail.split('\n').filter(Boolean);
155
- expect(lines.length).toBe(3);
156
- expect(lines[0]).toContain('Line 17');
157
- } finally {
158
- cleanup(tmpDir);
159
- }
160
- });
161
- });
162
-
163
- describe('learnings', () => {
164
- it('追加教训', async () => {
165
- const tmpDir = createTempDir();
166
- try {
167
- const gs = new GoalState(tmpDir);
168
- await gs.init('Test', []);
169
- await gs.appendLearning('Always backup before modifying settings.py');
170
- await gs.appendLearning('Use relative imports in routes/');
171
-
172
- expect(gs.learnings).toContain('backup before modifying settings.py');
173
- expect(gs.learnings).toContain('relative imports in routes/');
174
- } finally {
175
- cleanup(tmpDir);
176
- }
177
- });
178
-
179
- it('空 learnings 返回空字符串', async () => {
180
- const tmpDir = createTempDir();
181
- try {
182
- const gs = new GoalState(tmpDir);
183
- await gs.init('Test', []);
184
- expect(gs.learnings).toBe('');
185
- } finally {
186
- cleanup(tmpDir);
187
- }
188
- });
189
-
190
- it('learnings 超过 20 行时只返回尾部 20 行(防 context 膨胀)', async () => {
191
- const tmpDir = createTempDir();
192
- try {
193
- const gs = new GoalState(tmpDir);
194
- await gs.init('Test', []);
195
- for (let i = 0; i < 50; i++) {
196
- await gs.appendLearning(`lesson-${i}`);
197
- }
198
-
199
- const tail = gs.learnings;
200
- const lines = tail.split('\n').filter(Boolean);
201
- expect(lines.length).toBe(20);
202
- // 最早的应该已经被裁掉
203
- expect(tail).not.toContain('lesson-0\n');
204
- expect(tail).not.toContain('lesson-29\n');
205
- // 最新一条仍在
206
- expect(tail).toContain('lesson-49');
207
- } finally {
208
- cleanup(tmpDir);
209
- }
210
- });
211
- });
212
-
213
- describe('decision log', () => {
214
- it('记录决策', async () => {
215
- const tmpDir = createTempDir();
216
- try {
217
- const gs = new GoalState(tmpDir);
218
- await gs.init('Test', []);
219
- await gs.recordDecision(
220
- { iteration: 5, phase: Phase.PLAN, summary: 'Auth implementation approach' },
221
- ['JWT', 'Session', 'OAuth'],
222
- 'JWT',
223
- 'Single service, simpler for frontend',
224
- );
225
-
226
- const similar = gs.findSimilarDecisions(
227
- { iteration: 10, phase: Phase.PLAN, summary: 'Auth implementation approach' },
228
- );
229
- expect(similar).toHaveLength(1);
230
- expect(similar[0].chosen).toBe('JWT');
231
- } finally {
232
- cleanup(tmpDir);
233
- }
234
- });
235
-
236
- it('无相似决策时返回空数组', async () => {
237
- const tmpDir = createTempDir();
238
- try {
239
- const gs = new GoalState(tmpDir);
240
- await gs.init('Test', []);
241
- const similar = gs.findSimilarDecisions({
242
- iteration: 1,
243
- phase: Phase.PLAN,
244
- summary: 'Database choice',
245
- });
246
- expect(similar).toHaveLength(0);
247
- } finally {
248
- cleanup(tmpDir);
249
- }
250
- });
251
-
252
- it('summarizeDecisions 返回格式化摘要', async () => {
253
- const tmpDir = createTempDir();
254
- try {
255
- const gs = new GoalState(tmpDir);
256
- await gs.init('Test', []);
257
- await gs.recordDecision(
258
- { iteration: 3, phase: Phase.PLAN, summary: 'DB' },
259
- ['PostgreSQL', 'MySQL', 'SQLite'],
260
- 'PostgreSQL',
261
- 'JSON support needed',
262
- );
263
-
264
- const summary = gs.summarizeDecisions(5);
265
- expect(summary).toContain('PostgreSQL');
266
- expect(summary).toContain('迭代 3');
267
- } finally {
268
- cleanup(tmpDir);
269
- }
270
- });
271
- });
272
-
273
- describe('composeContext', () => {
274
- it('组装包含目标 + 阶段 + 本轮任务', async () => {
275
- const tmpDir = createTempDir();
276
- try {
277
- const gs = new GoalState(tmpDir);
278
- await gs.init('Build user auth module', []);
279
- const ctx = gs.composeContext(5);
280
-
281
- expect(ctx).toContain('不可变目标');
282
- expect(ctx).toContain('Build user auth module');
283
- expect(ctx).toContain('当前阶段: PLAN');
284
- expect(ctx).toContain('本轮任务 (迭代 5)');
285
- } finally {
286
- cleanup(tmpDir);
287
- }
288
- });
289
-
290
- it('composeContext 包含 learnings(如果有)', async () => {
291
- const tmpDir = createTempDir();
292
- try {
293
- const gs = new GoalState(tmpDir);
294
- await gs.init('Test', []);
295
- await gs.appendLearning('Lesson one');
296
-
297
- const ctx = gs.composeContext(1);
298
- expect(ctx).toContain('关键教训');
299
- expect(ctx).toContain('Lesson one');
300
- } finally {
301
- cleanup(tmpDir);
302
- }
303
- });
304
-
305
- it('composeContext 不包含空的 learnings 块', async () => {
306
- const tmpDir = createTempDir();
307
- try {
308
- const gs = new GoalState(tmpDir);
309
- await gs.init('Test', []);
310
-
311
- const ctx = gs.composeContext(1);
312
- expect(ctx).not.toContain('关键教训');
313
- } finally {
314
- cleanup(tmpDir);
315
- }
316
- });
317
- });
318
-
319
- describe('cleanup', () => {
320
- it('清理后文件不存在(await)', async () => {
321
- const tmpDir = createTempDir();
322
- try {
323
- const gs = new GoalState(tmpDir);
324
- await gs.init('Test', []);
325
- await gs.cleanup();
326
-
327
- expect(existsSync(join(tmpDir, '.minimal-agent', 'goal.md'))).toBe(false);
328
- } finally {
329
- cleanup(tmpDir);
330
- }
331
- });
332
-
333
- it('清理后空目录也被删除(await)', async () => {
334
- const tmpDir = createTempDir();
335
- try {
336
- const gs = new GoalState(tmpDir);
337
- await gs.init('Test', []);
338
- await gs.cleanup();
339
-
340
- expect(existsSync(join(tmpDir, '.minimal-agent'))).toBe(false);
341
- } finally {
342
- cleanup(tmpDir);
343
- }
344
- });
345
- });
346
-
347
- describe('reset', () => {
348
- it('reset 后旧目标被清除,新 init 写入新目标', async () => {
349
- const tmpDir = createTempDir();
350
- try {
351
- const gs = new GoalState(tmpDir);
352
- await gs.init('Old goal', []);
353
- expect(gs.goal).toContain('Old goal');
354
-
355
- await gs.reset();
356
- expect(gs.goal).toBe('');
357
-
358
- await gs.init('New goal', []);
359
- expect(gs.goal).toContain('New goal');
360
- } finally {
361
- cleanup(tmpDir);
362
- }
363
- });
364
- });
365
-
366
- describe('sessionTag', () => {
367
- it('不传 sessionTag 使用默认目录 .minimal-agent', async () => {
368
- const tmpDir = createTempDir();
369
- try {
370
- const gs = new GoalState(tmpDir);
371
- await gs.init('Test', []);
372
-
373
- expect(existsSync(join(tmpDir, '.minimal-agent'))).toBe(true);
374
- expect(existsSync(join(tmpDir, '.minimal-agent-session-a'))).toBe(false);
375
- } finally {
376
- cleanup(tmpDir);
377
- }
378
- });
379
-
380
- it('传入 sessionTag 使用带 tag 的目录', async () => {
381
- const tmpDir = createTempDir();
382
- try {
383
- const gs = new GoalState(tmpDir, 'session-a');
384
- await gs.init('Test', []);
385
-
386
- expect(existsSync(join(tmpDir, '.minimal-agent-session-a'))).toBe(true);
387
- expect(existsSync(join(tmpDir, '.minimal-agent-session-a', 'goal.md'))).toBe(true);
388
- expect(gs.goal).toContain('Test');
389
- } finally {
390
- cleanup(tmpDir);
391
- }
392
- });
393
-
394
- it('不同 sessionTag 隔离状态', async () => {
395
- const tmpDir = createTempDir();
396
- try {
397
- const gsA = new GoalState(tmpDir, 'alpha');
398
- const gsB = new GoalState(tmpDir, 'beta');
399
-
400
- await gsA.init('Goal Alpha', []);
401
- await gsB.init('Goal Beta', []);
402
-
403
- expect(gsA.goal).toContain('Goal Alpha');
404
- expect(gsB.goal).toContain('Goal Beta');
405
- } finally {
406
- cleanup(tmpDir);
407
- }
408
- });
409
- });
410
- });
@@ -1,122 +0,0 @@
1
- import { describe, expect, it } from 'bun:test';
2
- import {
3
- parseVerifyArgs,
4
- verifyFileExists,
5
- verifyFileContains,
6
- runVerification,
7
- } from '../src/verificationGate.ts';
8
- import type { Check } from '../src/verificationGate.ts';
9
-
10
- describe('verificationGate', () => {
11
- describe('parseVerifyArgs', () => {
12
- it('解析 --verify shell:xxx', () => {
13
- const checks = parseVerifyArgs('--verify "shell:npm test"');
14
- expect(checks).toHaveLength(1);
15
- expect(checks[0].type).toBe('shell');
16
- expect(checks[0].command).toBe('npm test');
17
- });
18
-
19
- it('解析多个 --verify', () => {
20
- const checks = parseVerifyArgs(
21
- '--verify "shell:npm test" --verify "file_exists:src/index.ts" --verify "file_contains:README.md:minimal-agent"',
22
- );
23
- expect(checks).toHaveLength(3);
24
- expect(checks[0].type).toBe('shell');
25
- expect(checks[1].type).toBe('file_exists');
26
- expect(checks[2].type).toBe('file_contains');
27
- });
28
-
29
- it('无 --verify 返回空数组', () => {
30
- const checks = parseVerifyArgs('--max-iterations 50 "some task"');
31
- expect(checks).toHaveLength(0);
32
- });
33
-
34
- it('空字符串返回空数组', () => {
35
- const checks = parseVerifyArgs('');
36
- expect(checks).toHaveLength(0);
37
- });
38
-
39
- it('忽略无效类型', () => {
40
- const checks = parseVerifyArgs('--verify "invalid:value"');
41
- expect(checks).toHaveLength(0);
42
- });
43
-
44
- it('解析 test_count', () => {
45
- const checks = parseVerifyArgs('--verify "test_count:10"');
46
- expect(checks).toHaveLength(1);
47
- expect(checks[0].type).toBe('test_count');
48
- expect(checks[0].minCount).toBe(10);
49
- });
50
- });
51
-
52
- describe('verifyFileExists', () => {
53
- it('存在的文件通过', () => {
54
- const result = verifyFileExists('package.json');
55
- expect(result.passed).toBe(true);
56
- expect(result.output).toContain('存在');
57
- });
58
-
59
- it('不存在的文件不通过', () => {
60
- const result = verifyFileExists('__nonexistent_file_xyz__');
61
- expect(result.passed).toBe(false);
62
- expect(result.output).toContain('不存在');
63
- });
64
- });
65
-
66
- describe('verifyFileContains', () => {
67
- it('文件包含模式串通过', () => {
68
- const result = verifyFileContains('package.json', '"name"');
69
- expect(result.passed).toBe(true);
70
- });
71
-
72
- it('文件不包含模式串不通过', () => {
73
- const result = verifyFileContains('package.json', '__xyz_pattern_not_exist__');
74
- expect(result.passed).toBe(false);
75
- });
76
-
77
- it('不存在的文件不通过', () => {
78
- const result = verifyFileContains('__nonexistent__', 'pattern');
79
- expect(result.passed).toBe(false);
80
- expect(result.output).toContain('无法读取');
81
- });
82
- });
83
-
84
- describe('runVerification', () => {
85
- it('无验证项直接通过', async () => {
86
- const result = await runVerification([]);
87
- expect(result.passed).toBe(true);
88
- expect(result.summary).toContain('无验证项');
89
- });
90
-
91
- it('全部通过的检查返回 passed=true', async () => {
92
- const checks: Check[] = [
93
- { type: 'file_exists', file: 'package.json' },
94
- { type: 'file_contains', file: 'package.json', pattern: '"name"' },
95
- ];
96
- const result = await runVerification(checks);
97
- expect(result.passed).toBe(true);
98
- expect(result.details).toHaveLength(2);
99
- expect(result.details.every((d) => d.passed)).toBe(true);
100
- });
101
-
102
- it('有失败的检查返回 passed=false', async () => {
103
- const checks: Check[] = [
104
- { type: 'file_exists', file: '__nonexistent__' },
105
- ];
106
- const result = await runVerification(checks);
107
- expect(result.passed).toBe(false);
108
- expect(result.summary).toContain('❌');
109
- });
110
-
111
- it('混合结果返回 passed=false', async () => {
112
- const checks: Check[] = [
113
- { type: 'file_exists', file: 'package.json' },
114
- { type: 'file_exists', file: '__nonexistent__' },
115
- ];
116
- const result = await runVerification(checks);
117
- expect(result.passed).toBe(false);
118
- expect(result.details[0].passed).toBe(true);
119
- expect(result.details[1].passed).toBe(false);
120
- });
121
- });
122
- });