@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.5

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 (246) hide show
  1. package/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
  2. package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
  3. package/dist/__tests__/engine.conditions-group.test.js +222 -0
  4. package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
  5. package/dist/__tests__/engine.conditions.test.d.ts +2 -0
  6. package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
  7. package/dist/__tests__/engine.conditions.test.js +164 -0
  8. package/dist/__tests__/engine.conditions.test.js.map +1 -0
  9. package/dist/__tests__/engine.resume.test.d.ts +2 -0
  10. package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
  11. package/dist/__tests__/engine.resume.test.js +93 -0
  12. package/dist/__tests__/engine.resume.test.js.map +1 -0
  13. package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
  14. package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
  15. package/dist/__tests__/engine.script-stage.test.js +108 -0
  16. package/dist/__tests__/engine.script-stage.test.js.map +1 -0
  17. package/dist/db/client.d.ts +3 -0
  18. package/dist/db/client.d.ts.map +1 -0
  19. package/{src/db/client.ts → dist/db/client.js} +1 -2
  20. package/dist/db/client.js.map +1 -0
  21. package/dist/engine.context-event.test.d.ts +2 -0
  22. package/dist/engine.context-event.test.d.ts.map +1 -0
  23. package/dist/engine.context-event.test.js +111 -0
  24. package/dist/engine.context-event.test.js.map +1 -0
  25. package/dist/engine.d.ts +52 -0
  26. package/dist/engine.d.ts.map +1 -0
  27. package/dist/engine.js +364 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/events.d.ts +163 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +12 -0
  32. package/dist/events.js.map +1 -0
  33. package/{src/index.ts → dist/index.d.ts} +3 -45
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +24 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/pipeline/agent-loader.d.ts +4 -0
  38. package/dist/pipeline/agent-loader.d.ts.map +1 -0
  39. package/dist/pipeline/agent-loader.js +28 -0
  40. package/dist/pipeline/agent-loader.js.map +1 -0
  41. package/dist/pipeline/agent-loader.test.d.ts +2 -0
  42. package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
  43. package/dist/pipeline/agent-loader.test.js +134 -0
  44. package/dist/pipeline/agent-loader.test.js.map +1 -0
  45. package/dist/pipeline/condition-evaluator.d.ts +6 -0
  46. package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
  47. package/dist/pipeline/condition-evaluator.js +105 -0
  48. package/dist/pipeline/condition-evaluator.js.map +1 -0
  49. package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
  50. package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
  51. package/dist/pipeline/condition-evaluator.test.js +103 -0
  52. package/dist/pipeline/condition-evaluator.test.js.map +1 -0
  53. package/dist/pipeline/context-pack-loader.d.ts +3 -0
  54. package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
  55. package/dist/pipeline/context-pack-loader.js +51 -0
  56. package/dist/pipeline/context-pack-loader.js.map +1 -0
  57. package/dist/pipeline/context-propagation.d.ts +28 -0
  58. package/dist/pipeline/context-propagation.d.ts.map +1 -0
  59. package/dist/pipeline/context-propagation.js +161 -0
  60. package/dist/pipeline/context-propagation.js.map +1 -0
  61. package/dist/pipeline/context-propagation.test.d.ts +2 -0
  62. package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
  63. package/dist/pipeline/context-propagation.test.js +194 -0
  64. package/dist/pipeline/context-propagation.test.js.map +1 -0
  65. package/dist/pipeline/contract-loader.d.ts +4 -0
  66. package/dist/pipeline/contract-loader.d.ts.map +1 -0
  67. package/dist/pipeline/contract-loader.js +30 -0
  68. package/dist/pipeline/contract-loader.js.map +1 -0
  69. package/dist/pipeline/group-orchestrator.d.ts +19 -0
  70. package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
  71. package/dist/pipeline/group-orchestrator.js +376 -0
  72. package/dist/pipeline/group-orchestrator.js.map +1 -0
  73. package/dist/pipeline/hook-executor.d.ts +28 -0
  74. package/dist/pipeline/hook-executor.d.ts.map +1 -0
  75. package/dist/pipeline/hook-executor.js +63 -0
  76. package/dist/pipeline/hook-executor.js.map +1 -0
  77. package/dist/pipeline/hook-executor.test.d.ts +2 -0
  78. package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
  79. package/dist/pipeline/hook-executor.test.js +71 -0
  80. package/dist/pipeline/hook-executor.test.js.map +1 -0
  81. package/dist/pipeline/invariants-loader.d.ts +6 -0
  82. package/dist/pipeline/invariants-loader.d.ts.map +1 -0
  83. package/dist/pipeline/invariants-loader.js +15 -0
  84. package/dist/pipeline/invariants-loader.js.map +1 -0
  85. package/dist/pipeline/invariants-loader.test.d.ts +2 -0
  86. package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
  87. package/dist/pipeline/invariants-loader.test.js +39 -0
  88. package/dist/pipeline/invariants-loader.test.js.map +1 -0
  89. package/dist/pipeline/loader.d.ts +5 -0
  90. package/dist/pipeline/loader.d.ts.map +1 -0
  91. package/dist/pipeline/loader.js +132 -0
  92. package/dist/pipeline/loader.js.map +1 -0
  93. package/dist/pipeline/loader.test.d.ts +2 -0
  94. package/dist/pipeline/loader.test.d.ts.map +1 -0
  95. package/dist/pipeline/loader.test.js +120 -0
  96. package/dist/pipeline/loader.test.js.map +1 -0
  97. package/dist/pipeline/output-validator.d.ts +10 -0
  98. package/dist/pipeline/output-validator.d.ts.map +1 -0
  99. package/dist/pipeline/output-validator.js +20 -0
  100. package/dist/pipeline/output-validator.js.map +1 -0
  101. package/dist/pipeline/output-validator.test.d.ts +2 -0
  102. package/dist/pipeline/output-validator.test.d.ts.map +1 -0
  103. package/dist/pipeline/output-validator.test.js +77 -0
  104. package/dist/pipeline/output-validator.test.js.map +1 -0
  105. package/dist/pipeline/post-validator.d.ts +8 -0
  106. package/dist/pipeline/post-validator.d.ts.map +1 -0
  107. package/dist/pipeline/post-validator.js +103 -0
  108. package/dist/pipeline/post-validator.js.map +1 -0
  109. package/dist/pipeline/skill-loader.d.ts +10 -0
  110. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  111. package/dist/pipeline/skill-loader.js +26 -0
  112. package/dist/pipeline/skill-loader.js.map +1 -0
  113. package/dist/pipeline/skill-loader.test.d.ts +2 -0
  114. package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
  115. package/dist/pipeline/skill-loader.test.js +38 -0
  116. package/dist/pipeline/skill-loader.test.js.map +1 -0
  117. package/dist/pipeline/stage-executor.d.ts +26 -0
  118. package/dist/pipeline/stage-executor.d.ts.map +1 -0
  119. package/dist/pipeline/stage-executor.js +540 -0
  120. package/dist/pipeline/stage-executor.js.map +1 -0
  121. package/dist/pipeline/stage-resolver.d.ts +3 -0
  122. package/dist/pipeline/stage-resolver.d.ts.map +1 -0
  123. package/dist/pipeline/stage-resolver.js +6 -0
  124. package/dist/pipeline/stage-resolver.js.map +1 -0
  125. package/dist/pipeline/startup-executor.d.ts +3 -0
  126. package/dist/pipeline/startup-executor.d.ts.map +1 -0
  127. package/dist/pipeline/startup-executor.js +23 -0
  128. package/dist/pipeline/startup-executor.js.map +1 -0
  129. package/dist/pipeline/startup-executor.test.d.ts +2 -0
  130. package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
  131. package/dist/pipeline/startup-executor.test.js +34 -0
  132. package/dist/pipeline/startup-executor.test.js.map +1 -0
  133. package/dist/pipeline/types.d.ts +28 -0
  134. package/dist/pipeline/types.d.ts.map +1 -0
  135. package/dist/pipeline/types.js +12 -0
  136. package/dist/pipeline/types.js.map +1 -0
  137. package/dist/repo-resolver.d.ts +10 -0
  138. package/dist/repo-resolver.d.ts.map +1 -0
  139. package/dist/repo-resolver.js +39 -0
  140. package/dist/repo-resolver.js.map +1 -0
  141. package/dist/spawners/direct-engine-spawner.d.ts +8 -0
  142. package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
  143. package/dist/spawners/direct-engine-spawner.js +24 -0
  144. package/dist/spawners/direct-engine-spawner.js.map +1 -0
  145. package/dist/state/run-store.d.ts +74 -0
  146. package/dist/state/run-store.d.ts.map +1 -0
  147. package/dist/state/run-store.js +269 -0
  148. package/dist/state/run-store.js.map +1 -0
  149. package/dist/state/run-store.test.d.ts +2 -0
  150. package/dist/state/run-store.test.d.ts.map +1 -0
  151. package/dist/state/run-store.test.js +130 -0
  152. package/dist/state/run-store.test.js.map +1 -0
  153. package/dist/state/state-machine.d.ts +6 -0
  154. package/dist/state/state-machine.d.ts.map +1 -0
  155. package/dist/state/state-machine.js +27 -0
  156. package/dist/state/state-machine.js.map +1 -0
  157. package/dist/state/status-derivation.d.ts +12 -0
  158. package/dist/state/status-derivation.d.ts.map +1 -0
  159. package/dist/state/status-derivation.js +30 -0
  160. package/dist/state/status-derivation.js.map +1 -0
  161. package/package.json +7 -4
  162. package/ARCHITECTURE.md +0 -57
  163. package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
  164. package/src/__tests__/engine.conditions-group.test.ts +0 -244
  165. package/src/__tests__/engine.conditions.test.ts +0 -186
  166. package/src/__tests__/engine.resume.test.ts +0 -108
  167. package/src/__tests__/engine.script-stage.test.ts +0 -125
  168. package/src/engine.context-event.test.ts +0 -175
  169. package/src/engine.ts +0 -491
  170. package/src/events.ts +0 -167
  171. package/src/pipeline/agent-loader.test.ts +0 -151
  172. package/src/pipeline/agent-loader.ts +0 -39
  173. package/src/pipeline/condition-evaluator.test.ts +0 -129
  174. package/src/pipeline/condition-evaluator.ts +0 -121
  175. package/src/pipeline/context-pack-loader.ts +0 -63
  176. package/src/pipeline/context-propagation.test.ts +0 -237
  177. package/src/pipeline/context-propagation.ts +0 -235
  178. package/src/pipeline/contract-loader.ts +0 -41
  179. package/src/pipeline/group-orchestrator.ts +0 -483
  180. package/src/pipeline/hook-executor.test.ts +0 -121
  181. package/src/pipeline/hook-executor.ts +0 -87
  182. package/src/pipeline/invariants-loader.test.ts +0 -51
  183. package/src/pipeline/invariants-loader.ts +0 -14
  184. package/src/pipeline/loader.test.ts +0 -128
  185. package/src/pipeline/loader.ts +0 -149
  186. package/src/pipeline/output-validator.test.ts +0 -100
  187. package/src/pipeline/output-validator.ts +0 -40
  188. package/src/pipeline/post-validator.ts +0 -124
  189. package/src/pipeline/skill-loader.test.ts +0 -44
  190. package/src/pipeline/skill-loader.ts +0 -32
  191. package/src/pipeline/stage-executor.ts +0 -654
  192. package/src/pipeline/stage-resolver.ts +0 -8
  193. package/src/pipeline/startup-executor.test.ts +0 -37
  194. package/src/pipeline/startup-executor.ts +0 -32
  195. package/src/pipeline/types.ts +0 -42
  196. package/src/repo-resolver.ts +0 -61
  197. package/src/spawners/direct-engine-spawner.ts +0 -26
  198. package/src/state/run-store.test.ts +0 -157
  199. package/src/state/run-store.ts +0 -362
  200. package/src/state/state-machine.ts +0 -35
  201. package/src/state/status-derivation.ts +0 -36
  202. package/tests/context-pack-loader.test.ts +0 -113
  203. package/tests/context-propagation.test.ts +0 -267
  204. package/tests/direct-engine-spawner.test.ts +0 -102
  205. package/tests/e2e/feature-v5.test.ts +0 -57
  206. package/tests/events.test.ts +0 -56
  207. package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
  208. package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
  209. package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
  210. package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
  211. package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
  212. package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
  213. package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
  214. package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
  215. package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
  216. package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
  217. package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
  218. package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
  219. package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
  220. package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
  221. package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
  222. package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
  223. package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
  224. package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
  225. package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
  226. package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
  227. package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
  228. package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
  229. package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
  230. package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
  231. package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
  232. package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
  233. package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
  234. package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
  235. package/tests/loader.test.ts +0 -385
  236. package/tests/post-validator.test.ts +0 -297
  237. package/tests/repo-resolver.test.ts +0 -102
  238. package/tests/run-store.test.ts +0 -143
  239. package/tests/state-machine.test.ts +0 -110
  240. package/tests/unit/context-propagation.test.ts +0 -45
  241. package/tests/unit/engine.test.ts +0 -770
  242. package/tests/unit/group-loop.test.ts +0 -389
  243. package/tests/unit/group-parallel.test.ts +0 -423
  244. package/tests/unit/state/status-derivation.test.ts +0 -88
  245. package/tsconfig.json +0 -24
  246. package/vitest.config.ts +0 -14
@@ -1,385 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { join } from 'node:path';
3
- import { parsePipelineYaml, loadPipeline, loadPipelineByName } from '../src/pipeline/loader.js';
4
- import { parseAgentYaml } from '../src/pipeline/agent-loader.js';
5
- import { parseContractYaml } from '../src/pipeline/contract-loader.js';
6
- import { isStageGroup } from '@studio-foundation/contracts';
7
-
8
- const FIXTURES_DIR = join(import.meta.dirname, 'fixtures');
9
- const PIPELINES_DIR = join(FIXTURES_DIR, 'software', 'pipelines');
10
-
11
- describe('parsePipelineYaml', () => {
12
- it('parses a valid pipeline YAML', () => {
13
- const yamlContent = `
14
- name: test-pipeline
15
- description: A test pipeline
16
- version: 1
17
- stages:
18
- - name: stage-1
19
- kind: analysis
20
- agent: analyst
21
- ralph:
22
- max_attempts: 3
23
- `;
24
- const pipeline = parsePipelineYaml(yamlContent);
25
- expect(pipeline.name).toBe('test-pipeline');
26
- expect(pipeline.stages).toHaveLength(1);
27
- expect(pipeline.stages[0].name).toBe('stage-1');
28
- expect(pipeline.stages[0].kind).toBe('analysis');
29
- expect(pipeline.stages[0].agent).toBe('analyst');
30
- });
31
-
32
- it('throws when name is missing', () => {
33
- const yamlContent = `
34
- stages:
35
- - name: s1
36
- kind: analysis
37
- agent: a
38
- `;
39
- expect(() => parsePipelineYaml(yamlContent)).toThrow("missing required field 'name'");
40
- });
41
-
42
- it('throws when stages is missing', () => {
43
- const yamlContent = `
44
- name: bad-pipeline
45
- `;
46
- expect(() => parsePipelineYaml(yamlContent)).toThrow("missing required field 'stages'");
47
- });
48
-
49
- it('throws when stages is empty', () => {
50
- const yamlContent = `
51
- name: empty-pipeline
52
- stages: []
53
- `;
54
- expect(() => parsePipelineYaml(yamlContent)).toThrow('at least one stage');
55
- });
56
-
57
- it('throws when a stage is missing kind', () => {
58
- const yamlContent = `
59
- name: bad-stages
60
- stages:
61
- - name: no-kind
62
- agent: analyst
63
- `;
64
- expect(() => parsePipelineYaml(yamlContent)).toThrow("missing 'kind'");
65
- });
66
-
67
- it('parses pipeline with repo config', () => {
68
- const yamlContent = `
69
- name: test-pipeline
70
- description: Test
71
- version: 1
72
- repo:
73
- url: https://github.com/test/repo
74
- branch: main
75
- stages:
76
- - name: stage1
77
- kind: analysis
78
- agent: test-agent
79
- `;
80
- const pipeline = parsePipelineYaml(yamlContent);
81
- expect(pipeline.repo?.url).toBe('https://github.com/test/repo');
82
- expect(pipeline.repo?.branch).toBe('main');
83
- });
84
-
85
- it('parses pipeline without repo config', () => {
86
- const yamlContent = `
87
- name: test-pipeline
88
- description: Test
89
- version: 1
90
- stages:
91
- - name: stage1
92
- kind: analysis
93
- agent: test-agent
94
- `;
95
- const pipeline = parsePipelineYaml(yamlContent);
96
- expect(pipeline.repo).toBeUndefined();
97
- });
98
-
99
- it('throws when a stage is missing agent', () => {
100
- const yamlContent = `
101
- name: bad-stages
102
- stages:
103
- - name: no-agent
104
- kind: analysis
105
- `;
106
- expect(() => parsePipelineYaml(yamlContent)).toThrow("missing 'agent'");
107
- });
108
-
109
- it('parses a pipeline with a group entry', () => {
110
- const yamlContent = `
111
- name: grouped-pipeline
112
- description: Pipeline with a group
113
- version: 2
114
- stages:
115
- - name: analysis
116
- kind: analysis
117
- agent: analyst
118
- - group: review-loop
119
- max_iterations: 3
120
- stages:
121
- - name: code-gen
122
- kind: code_generation
123
- agent: coder
124
- - name: qa
125
- kind: qa
126
- agent: analyst
127
- `;
128
- const pipeline = parsePipelineYaml(yamlContent);
129
- expect(pipeline.stages).toHaveLength(2);
130
-
131
- // First entry is a regular stage
132
- const first = pipeline.stages[0];
133
- expect(isStageGroup(first)).toBe(false);
134
- expect((first as any).name).toBe('analysis');
135
-
136
- // Second entry is a group
137
- const second = pipeline.stages[1];
138
- expect(isStageGroup(second)).toBe(true);
139
- if (isStageGroup(second)) {
140
- expect(second.group).toBe('review-loop');
141
- expect(second.max_iterations).toBe(3);
142
- expect(second.stages).toHaveLength(2);
143
- expect(second.stages[0].name).toBe('code-gen');
144
- expect(second.stages[1].name).toBe('qa');
145
- }
146
- });
147
-
148
- it('parses max_tool_calls from the ralph block', () => {
149
- const yamlContent = `
150
- name: test-pipeline
151
- version: 1
152
- stages:
153
- - name: code-gen
154
- kind: code_generation
155
- agent: coder
156
- ralph:
157
- max_attempts: 5
158
- max_tool_calls: 200
159
- `;
160
- const pipeline = parsePipelineYaml(yamlContent);
161
- const stage = pipeline.stages[0] as any;
162
- expect(stage.ralph?.max_tool_calls).toBe(200);
163
- });
164
-
165
- it('leaves max_tool_calls undefined when not specified', () => {
166
- const yamlContent = `
167
- name: test-pipeline
168
- version: 1
169
- stages:
170
- - name: brief-analysis
171
- kind: analysis
172
- agent: analyst
173
- ralph:
174
- max_attempts: 3
175
- `;
176
- const pipeline = parsePipelineYaml(yamlContent);
177
- const stage = pipeline.stages[0] as any;
178
- expect(stage.ralph?.max_tool_calls).toBeUndefined();
179
- });
180
-
181
- it('defaults max_iterations to 3 when not specified', () => {
182
- const yamlContent = `
183
- name: default-iters
184
- version: 1
185
- stages:
186
- - group: loop
187
- stages:
188
- - name: s1
189
- kind: analysis
190
- agent: a
191
- - name: s2
192
- kind: qa
193
- agent: a
194
- `;
195
- const pipeline = parsePipelineYaml(yamlContent);
196
- const g = pipeline.stages[0];
197
- expect(isStageGroup(g)).toBe(true);
198
- if (isStageGroup(g)) {
199
- expect(g.max_iterations).toBe(3);
200
- }
201
- });
202
-
203
- it('throws when a group has no stages', () => {
204
- const yamlContent = `
205
- name: bad-group
206
- version: 1
207
- stages:
208
- - group: empty-loop
209
- stages: []
210
- `;
211
- expect(() => parsePipelineYaml(yamlContent)).toThrow('at least 2 stages');
212
- });
213
-
214
- it('throws when a group has only 1 stage', () => {
215
- const yamlContent = `
216
- name: bad-group
217
- version: 1
218
- stages:
219
- - group: solo-loop
220
- stages:
221
- - name: only-one
222
- kind: analysis
223
- agent: a
224
- `;
225
- expect(() => parsePipelineYaml(yamlContent)).toThrow('at least 2 stages');
226
- });
227
- });
228
-
229
- describe('loadPipeline — real files', () => {
230
- it('loads feature-builder.pipeline.yaml', async () => {
231
- const pipeline = await loadPipeline(join(PIPELINES_DIR, 'feature-builder.pipeline.yaml'));
232
- expect(pipeline.name).toBe('feature-builder');
233
- // 2 simple stages + 1 group = 3 entries
234
- expect(pipeline.stages).toHaveLength(3);
235
- });
236
-
237
- it('loadPipelineByName resolves the path', async () => {
238
- const pipeline = await loadPipelineByName('feature-builder', PIPELINES_DIR);
239
- expect(pipeline.name).toBe('feature-builder');
240
- });
241
-
242
- it('throws for non-existent pipeline', async () => {
243
- await expect(
244
- loadPipeline('/nonexistent/path/pipeline.yaml')
245
- ).rejects.toThrow('Failed to load pipeline');
246
- });
247
- });
248
-
249
- describe('parseAgentYaml', () => {
250
- it('parses a valid agent YAML', () => {
251
- const yamlContent = `
252
- name: test-agent
253
- provider: anthropic
254
- model: claude-sonnet-4-20250514
255
- temperature: 0.3
256
- tools:
257
- - repo_manager.read_file
258
- system_prompt: You are a test agent.
259
- `;
260
- const agent = parseAgentYaml(yamlContent);
261
- expect(agent.name).toBe('test-agent');
262
- expect(agent.provider).toBe('anthropic');
263
- expect(agent.model).toBe('claude-sonnet-4-20250514');
264
- });
265
-
266
- it('throws when name is missing', () => {
267
- expect(() => parseAgentYaml('provider: x\nmodel: y')).toThrow("missing required field 'name'");
268
- });
269
-
270
- it('allows missing provider (defaults applied at runtime)', () => {
271
- const agent = parseAgentYaml('name: x\nmodel: y');
272
- expect(agent.name).toBe('x');
273
- expect(agent.provider).toBeUndefined();
274
- });
275
-
276
- it('allows missing model (defaults applied at runtime)', () => {
277
- const agent = parseAgentYaml('name: x\nprovider: y');
278
- expect(agent.name).toBe('x');
279
- expect(agent.model).toBeUndefined();
280
- });
281
- });
282
-
283
- describe('parseContractYaml', () => {
284
- it('parses a valid contract YAML', () => {
285
- const yamlContent = `
286
- name: test-contract
287
- version: 1
288
- schema:
289
- required_fields:
290
- - summary
291
- - details
292
- `;
293
- const contract = parseContractYaml(yamlContent);
294
- expect(contract.name).toBe('test-contract');
295
- expect(contract.version).toBe(1);
296
- expect(contract.schema?.required_fields).toEqual(['summary', 'details']);
297
- });
298
-
299
- it('throws when name is missing', () => {
300
- expect(() => parseContractYaml('version: 1')).toThrow("missing required field 'name'");
301
- });
302
-
303
- it('throws when version is missing', () => {
304
- expect(() => parseContractYaml('name: x')).toThrow("missing required field 'version'");
305
- });
306
- });
307
-
308
- describe('parsePipelineYaml — parallel group', () => {
309
- it('parses mode: parallel and on_failure: collect-all', () => {
310
- const yaml = `
311
- name: parallel-test
312
- description: parallel
313
- version: 1
314
- stages:
315
- - group: work
316
- mode: parallel
317
- on_failure: collect-all
318
- stages:
319
- - name: stage-a
320
- kind: analysis
321
- agent: analyst
322
- - name: stage-b
323
- kind: analysis
324
- agent: analyst
325
- `;
326
- const pipeline = parsePipelineYaml(yaml);
327
- const group = pipeline.stages[0];
328
- expect(isStageGroup(group)).toBe(true);
329
- if (isStageGroup(group)) {
330
- expect(group.mode).toBe('parallel');
331
- expect(group.on_failure).toBe('collect-all');
332
- }
333
- });
334
-
335
- it('defaults mode to sequential when omitted', () => {
336
- const yaml = `
337
- name: seq-test
338
- description: seq
339
- version: 1
340
- stages:
341
- - group: work
342
- stages:
343
- - name: stage-a
344
- kind: analysis
345
- agent: analyst
346
- - name: stage-b
347
- kind: analysis
348
- agent: analyst
349
- `;
350
- const pipeline = parsePipelineYaml(yaml);
351
- const group = pipeline.stages[0];
352
- if (isStageGroup(group)) {
353
- expect(group.mode).toBeUndefined();
354
- }
355
- });
356
-
357
- it('warns and sets max_iterations to 1 when mode is parallel and max_iterations > 1', () => {
358
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
359
- const yaml = `
360
- name: parallel-warn-test
361
- description: parallel warn
362
- version: 1
363
- stages:
364
- - group: work
365
- mode: parallel
366
- max_iterations: 3
367
- stages:
368
- - name: stage-a
369
- kind: analysis
370
- agent: analyst
371
- - name: stage-b
372
- kind: analysis
373
- agent: analyst
374
- `;
375
- const pipeline = parsePipelineYaml(yaml);
376
- const group = pipeline.stages[0];
377
- if (isStageGroup(group)) {
378
- expect(group.max_iterations).toBe(1);
379
- expect(consoleSpy).toHaveBeenCalledWith(
380
- expect.stringContaining("parallel group 'work' has max_iterations > 1")
381
- );
382
- }
383
- consoleSpy.mockRestore();
384
- });
385
- });
@@ -1,297 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { postValidate } from '../src/pipeline/post-validator.js';
3
- import type { OutputContract } from '@studio-foundation/contracts';
4
-
5
- describe('postValidate', () => {
6
- it('accepts when no post_validation config', () => {
7
- const contract: OutputContract = { name: 'test', version: 1 };
8
- const result = postValidate({ status: 'rejected' }, contract);
9
- expect(result.accepted).toBe(true);
10
- });
11
-
12
- it('accepts when status is in approved_values', () => {
13
- const contract: OutputContract = {
14
- name: 'qa',
15
- version: 1,
16
- post_validation: {
17
- rejection_detection: { field: 'status', approved_values: ['approved'] },
18
- },
19
- };
20
- const result = postValidate({ status: 'approved', summary: 'ok', issues: [] }, contract);
21
- expect(result.accepted).toBe(true);
22
- });
23
-
24
- it('accepts with approved_with_notes', () => {
25
- const contract: OutputContract = {
26
- name: 'qa',
27
- version: 1,
28
- post_validation: {
29
- rejection_detection: { field: 'status', approved_values: ['approved', 'approved_with_notes'] },
30
- },
31
- };
32
- const result = postValidate({ status: 'approved_with_notes', summary: 'minor issues', issues: [] }, contract);
33
- expect(result.accepted).toBe(true);
34
- });
35
-
36
- it('rejects when status is not in approved_values', () => {
37
- const contract: OutputContract = {
38
- name: 'qa',
39
- version: 1,
40
- post_validation: {
41
- rejection_detection: {
42
- field: 'status',
43
- approved_values: ['approved', 'approved_with_notes'],
44
- details_field: 'issues',
45
- summary_field: 'summary',
46
- },
47
- },
48
- };
49
- const result = postValidate(
50
- { status: 'implementation_incomplete', summary: 'missing props', issues: ['bug 1', 'bug 2'] },
51
- contract,
52
- );
53
- expect(result.accepted).toBe(false);
54
- expect(result.rejection_reason).toContain('implementation_incomplete');
55
- expect(result.rejection_reason).toContain('missing props');
56
- expect(result.rejection_details).toEqual(['bug 1', 'bug 2']);
57
- });
58
-
59
- it('extracts details from structured issues', () => {
60
- const contract: OutputContract = {
61
- name: 'qa',
62
- version: 1,
63
- post_validation: {
64
- rejection_detection: {
65
- field: 'status',
66
- approved_values: ['approved'],
67
- details_field: 'issues',
68
- },
69
- },
70
- };
71
- const result = postValidate(
72
- {
73
- status: 'needs_changes',
74
- summary: 'issues found',
75
- issues: [
76
- { severity: 'high', description: 'Missing prop' },
77
- { severity: 'low', description: 'Naming convention' },
78
- ],
79
- },
80
- contract,
81
- );
82
- expect(result.accepted).toBe(false);
83
- expect(result.rejection_details).toEqual(['Missing prop', 'Naming convention']);
84
- });
85
-
86
- it('accepts when output is not an object', () => {
87
- const contract: OutputContract = {
88
- name: 'qa',
89
- version: 1,
90
- post_validation: {
91
- rejection_detection: { field: 'status', approved_values: ['approved'] },
92
- },
93
- };
94
- expect(postValidate('just a string', contract).accepted).toBe(true);
95
- expect(postValidate(null, contract).accepted).toBe(true);
96
- expect(postValidate(undefined, contract).accepted).toBe(true);
97
- });
98
-
99
- it('accepts when status field is not a string', () => {
100
- const contract: OutputContract = {
101
- name: 'qa',
102
- version: 1,
103
- post_validation: {
104
- rejection_detection: { field: 'status', approved_values: ['approved'] },
105
- },
106
- };
107
- const result = postValidate({ status: 42, summary: 'ok' }, contract);
108
- expect(result.accepted).toBe(true);
109
- });
110
-
111
- it('includes expected values in rejection reason', () => {
112
- const contract: OutputContract = {
113
- name: 'qa',
114
- version: 1,
115
- post_validation: {
116
- rejection_detection: { field: 'status', approved_values: ['approved', 'approved_with_notes'] },
117
- },
118
- };
119
- const result = postValidate({ status: 'failed', issues: [] }, contract);
120
- expect(result.accepted).toBe(false);
121
- expect(result.rejection_reason).toContain('approved or approved_with_notes');
122
- });
123
-
124
- it('handles string issues field', () => {
125
- const contract: OutputContract = {
126
- name: 'qa',
127
- version: 1,
128
- post_validation: {
129
- rejection_detection: {
130
- field: 'status',
131
- approved_values: ['approved'],
132
- details_field: 'issues',
133
- },
134
- },
135
- };
136
- const result = postValidate(
137
- { status: 'rejected', issues: 'Something is wrong' },
138
- contract,
139
- );
140
- expect(result.accepted).toBe(false);
141
- expect(result.rejection_details).toEqual(['Something is wrong']);
142
- });
143
-
144
- it('rejects when reject_if_non_empty field has items, even if status is approved', () => {
145
- const contract: OutputContract = {
146
- name: 'critique',
147
- version: 1,
148
- post_validation: {
149
- rejection_detection: {
150
- field: 'status',
151
- approved_values: ['approved'],
152
- details_field: 'issues',
153
- summary_field: 'feedback',
154
- reject_if_non_empty: 'issues',
155
- },
156
- },
157
- };
158
- const result = postValidate(
159
- { status: 'approved', feedback: 'Minor issues', issues: ['Too much salt'] },
160
- contract,
161
- );
162
- expect(result.accepted).toBe(false);
163
- expect(result.rejection_reason).toContain('non-empty');
164
- expect(result.rejection_reason).toContain('Minor issues');
165
- expect(result.rejection_details).toEqual(['Too much salt']);
166
- });
167
-
168
- it('accepts when reject_if_non_empty field is an empty array', () => {
169
- const contract: OutputContract = {
170
- name: 'critique',
171
- version: 1,
172
- post_validation: {
173
- rejection_detection: {
174
- field: 'status',
175
- approved_values: ['approved'],
176
- reject_if_non_empty: 'issues',
177
- },
178
- },
179
- };
180
- const result = postValidate(
181
- { status: 'approved', issues: [] },
182
- contract,
183
- );
184
- expect(result.accepted).toBe(true);
185
- });
186
-
187
- it('rejects with structured issues via reject_if_non_empty', () => {
188
- const contract: OutputContract = {
189
- name: 'critique',
190
- version: 1,
191
- post_validation: {
192
- rejection_detection: {
193
- field: 'status',
194
- approved_values: ['approved'],
195
- reject_if_non_empty: 'issues',
196
- },
197
- },
198
- };
199
- const result = postValidate(
200
- { status: 'approved', issues: [{ severity: 'low', description: 'Bland seasoning' }] },
201
- contract,
202
- );
203
- expect(result.accepted).toBe(false);
204
- expect(result.rejection_details).toEqual(['Bland seasoning']);
205
- });
206
-
207
- it('extracts details from objects using "issue" field when "description" is absent', () => {
208
- const contract: OutputContract = {
209
- name: 'qa',
210
- version: 1,
211
- post_validation: {
212
- rejection_detection: {
213
- field: 'status',
214
- approved_values: ['approved'],
215
- details_field: 'issues',
216
- },
217
- },
218
- };
219
- const result = postValidate(
220
- {
221
- status: 'rejected',
222
- issues: [
223
- { issue: 'Missing error handling', severity: 'high' },
224
- { issue: 'No input validation' },
225
- ],
226
- },
227
- contract,
228
- );
229
- expect(result.accepted).toBe(false);
230
- expect(result.rejection_details).toEqual(['Missing error handling', 'No input validation']);
231
- });
232
-
233
- it('extracts details from objects using "message" field as fallback', () => {
234
- const contract: OutputContract = {
235
- name: 'qa',
236
- version: 1,
237
- post_validation: {
238
- rejection_detection: {
239
- field: 'status',
240
- approved_values: ['approved'],
241
- details_field: 'issues',
242
- },
243
- },
244
- };
245
- const result = postValidate(
246
- {
247
- status: 'rejected',
248
- issues: [
249
- { message: 'Auth token not refreshed', file: 'src/auth.ts' },
250
- ],
251
- },
252
- contract,
253
- );
254
- expect(result.accepted).toBe(false);
255
- expect(result.rejection_details).toEqual(['Auth token not refreshed']);
256
- });
257
-
258
- it('falls back to first string value for objects with unknown field names', () => {
259
- const contract: OutputContract = {
260
- name: 'qa',
261
- version: 1,
262
- post_validation: {
263
- rejection_detection: {
264
- field: 'status',
265
- approved_values: ['approved'],
266
- details_field: 'issues',
267
- },
268
- },
269
- };
270
- const result = postValidate(
271
- {
272
- status: 'rejected',
273
- issues: [
274
- { problem: 'Missing test coverage', severity: 'medium' },
275
- ],
276
- },
277
- contract,
278
- );
279
- expect(result.accepted).toBe(false);
280
- expect(result.rejection_details).toHaveLength(1);
281
- // Should extract some string value from the object
282
- expect(typeof result.rejection_details![0]).toBe('string');
283
- });
284
-
285
- it('omits rejection_details when no details_field configured', () => {
286
- const contract: OutputContract = {
287
- name: 'qa',
288
- version: 1,
289
- post_validation: {
290
- rejection_detection: { field: 'status', approved_values: ['approved'] },
291
- },
292
- };
293
- const result = postValidate({ status: 'rejected' }, contract);
294
- expect(result.accepted).toBe(false);
295
- expect(result.rejection_details).toBeUndefined();
296
- });
297
- });