@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,389 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { join } from 'node:path';
3
- import { PipelineEngine, type EngineConfig } from '../../src/engine.js';
4
- import { InMemoryRunStore } from '../../src/state/run-store.js';
5
- import type { EngineEvents } from '../../src/events.js';
6
- import { mkdirSync, writeFileSync } from 'node:fs';
7
-
8
- const FIXTURES_DIR = join(import.meta.dirname, '..', 'fixtures');
9
- const PROJECT_DIR = join(FIXTURES_DIR, 'test-project');
10
- const PIPELINES_DIR = join(PROJECT_DIR, 'pipelines');
11
- const AGENTS_DIR = join(PROJECT_DIR, 'agents');
12
- const CONTRACTS_DIR = join(PROJECT_DIR, 'contracts');
13
-
14
- // Ensure fixture dirs exist (shared with engine.test.ts)
15
- mkdirSync(PIPELINES_DIR, { recursive: true });
16
- mkdirSync(AGENTS_DIR, { recursive: true });
17
- mkdirSync(CONTRACTS_DIR, { recursive: true });
18
-
19
- // Agent fixture (reuse existing)
20
- writeFileSync(join(AGENTS_DIR, 'test-agent.agent.yaml'), `
21
- name: test-agent
22
- provider: anthropic
23
- model: claude-sonnet-4-20250514
24
- temperature: 0.3
25
- `);
26
-
27
- // Contract with approval gate for QA
28
- writeFileSync(join(CONTRACTS_DIR, 'qa-gate.contract.yaml'), `
29
- name: qa-gate
30
- version: 1
31
- schema:
32
- required_fields:
33
- - status
34
- - issues
35
- post_validation:
36
- rejection_detection:
37
- field: status
38
- approved_values:
39
- - approved
40
- - pass
41
- details_field: issues
42
- summary_field: summary
43
- `);
44
-
45
- // Contract without approval (for code-gen stage)
46
- writeFileSync(join(CONTRACTS_DIR, 'code-gen.contract.yaml'), `
47
- name: code-gen
48
- version: 1
49
- schema:
50
- required_fields:
51
- - files_changed
52
- `);
53
-
54
- // Pipeline with a group
55
- writeFileSync(join(PIPELINES_DIR, 'group-test.pipeline.yaml'), `
56
- name: group-test
57
- description: Test pipeline with a feedback loop group
58
- version: 2
59
- stages:
60
- - name: analysis
61
- kind: analysis
62
- agent: test-agent
63
- ralph:
64
- max_attempts: 1
65
- retry_strategy: none
66
- context:
67
- include:
68
- - input
69
- - group: impl-review
70
- max_iterations: 3
71
- stages:
72
- - name: code-gen
73
- kind: code_generation
74
- agent: test-agent
75
- contract: code-gen
76
- ralph:
77
- max_attempts: 1
78
- retry_strategy: none
79
- context:
80
- include:
81
- - input
82
- - all_stage_outputs
83
- - group_feedback
84
- - name: qa-review
85
- kind: qa
86
- agent: test-agent
87
- contract: qa-gate
88
- ralph:
89
- max_attempts: 1
90
- retry_strategy: none
91
- context:
92
- include:
93
- - input
94
- - all_stage_outputs
95
- `);
96
-
97
- function createMockProvider(callFn: (...args: any[]) => any) {
98
- return {
99
- name: 'anthropic',
100
- call: vi.fn(callFn),
101
- };
102
- }
103
-
104
- function createMockToolRegistry() {
105
- return {
106
- register: vi.fn(),
107
- get: vi.fn(),
108
- has: vi.fn().mockReturnValue(false),
109
- list: vi.fn().mockReturnValue([]),
110
- toToolDefinitions: vi.fn().mockReturnValue([]),
111
- filter: vi.fn().mockReturnThis(),
112
- getActiveSnippets: vi.fn().mockReturnValue([]),
113
- };
114
- }
115
-
116
- function createEngine(provider: any, events?: EngineEvents): PipelineEngine {
117
- return new PipelineEngine(
118
- {
119
- configsDir: PROJECT_DIR,
120
- providerRegistry: { get: vi.fn().mockReturnValue(provider), register: vi.fn() } as any,
121
- toolRegistry: createMockToolRegistry() as any,
122
- db: new InMemoryRunStore(),
123
- },
124
- events
125
- );
126
- }
127
-
128
- // Response helpers
129
- function analysisResponse() {
130
- return {
131
- content: JSON.stringify({
132
- summary: 'Analysis done',
133
- requirements: ['r1'],
134
- acceptance_criteria: ['ac1'],
135
- }),
136
- tool_calls: [],
137
- finish_reason: 'stop',
138
- usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
139
- };
140
- }
141
-
142
- function codeGenResponse() {
143
- return {
144
- content: JSON.stringify({
145
- files_changed: ['src/app.ts'],
146
- }),
147
- tool_calls: [],
148
- finish_reason: 'stop',
149
- usage: { prompt_tokens: 200, completion_tokens: 100, total_tokens: 300 },
150
- };
151
- }
152
-
153
- function qaApproveResponse() {
154
- return {
155
- content: JSON.stringify({
156
- status: 'approved',
157
- issues: [],
158
- }),
159
- tool_calls: [],
160
- finish_reason: 'stop',
161
- usage: { prompt_tokens: 150, completion_tokens: 50, total_tokens: 200 },
162
- };
163
- }
164
-
165
- function qaRejectResponse(reason: string, issues: string[]) {
166
- return {
167
- content: JSON.stringify({
168
- status: 'needs_changes',
169
- summary: reason,
170
- issues,
171
- }),
172
- tool_calls: [],
173
- finish_reason: 'stop',
174
- usage: { prompt_tokens: 150, completion_tokens: 50, total_tokens: 200 },
175
- };
176
- }
177
-
178
- describe('Group feedback loop', () => {
179
- it('succeeds on first iteration when QA approves', async () => {
180
- let callCount = 0;
181
- const provider = createMockProvider(() => {
182
- callCount++;
183
- if (callCount === 1) return analysisResponse();
184
- if (callCount === 2) return codeGenResponse();
185
- return qaApproveResponse();
186
- });
187
-
188
- const engine = createEngine(provider);
189
- const result = await engine.run({ pipeline: 'group-test', input: 'Build feature' });
190
-
191
- expect(result.status).toBe('success');
192
- // 3 stage runs: analysis + code-gen + qa-review
193
- expect(result.stages).toHaveLength(3);
194
- expect(result.stages[0].stage_name).toBe('analysis');
195
- expect(result.stages[1].stage_name).toBe('code-gen');
196
- expect(result.stages[2].stage_name).toBe('qa-review');
197
- expect(provider.call).toHaveBeenCalledTimes(3);
198
- });
199
-
200
- it('succeeds on second iteration after QA rejection', async () => {
201
- let callCount = 0;
202
- const provider = createMockProvider(() => {
203
- callCount++;
204
- if (callCount === 1) return analysisResponse(); // analysis
205
- if (callCount === 2) return codeGenResponse(); // code-gen iter 1
206
- if (callCount === 3) return qaRejectResponse( // qa rejects iter 1
207
- 'Missing error handling',
208
- ['No try-catch around API call', 'Missing null check']
209
- );
210
- if (callCount === 4) return codeGenResponse(); // code-gen iter 2
211
- return qaApproveResponse(); // qa approves iter 2
212
- });
213
-
214
- const engine = createEngine(provider);
215
- const result = await engine.run({ pipeline: 'group-test', input: 'Build feature' });
216
-
217
- expect(result.status).toBe('success');
218
- // 5 stage runs: analysis + (code-gen + qa) × 2
219
- expect(result.stages).toHaveLength(5);
220
- expect(provider.call).toHaveBeenCalledTimes(5);
221
- });
222
-
223
- it('rejects after max_iterations exhausted', async () => {
224
- let callCount = 0;
225
- const provider = createMockProvider(() => {
226
- callCount++;
227
- if (callCount === 1) return analysisResponse();
228
- // Alternate code-gen / qa-reject forever
229
- if (callCount % 2 === 0) return codeGenResponse();
230
- return qaRejectResponse('Still broken', ['Issue persists']);
231
- });
232
-
233
- const engine = createEngine(provider);
234
- const result = await engine.run({ pipeline: 'group-test', input: 'Build feature' });
235
-
236
- expect(result.status).toBe('rejected');
237
- // 1 analysis + 3 × (code-gen + qa) = 7
238
- expect(result.stages).toHaveLength(7);
239
- expect(provider.call).toHaveBeenCalledTimes(7);
240
- });
241
-
242
- it('fails immediately on technical failure in group', async () => {
243
- let callCount = 0;
244
- const provider = createMockProvider(() => {
245
- callCount++;
246
- if (callCount === 1) return analysisResponse();
247
- // code-gen returns invalid output (missing required field)
248
- return {
249
- content: JSON.stringify({ no_files_changed: true }),
250
- tool_calls: [],
251
- finish_reason: 'stop',
252
- usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
253
- };
254
- });
255
-
256
- const engine = createEngine(provider);
257
- const result = await engine.run({ pipeline: 'group-test', input: 'Build feature' });
258
-
259
- expect(result.status).toBe('failed');
260
- // Only 2 stage runs: analysis + code-gen (failed, no qa)
261
- expect(result.stages).toHaveLength(2);
262
- });
263
-
264
- it('preserves pre-group outputs across iterations', async () => {
265
- // Track what context the code-gen agent receives on iteration 2
266
- let callCount = 0;
267
- const callArgs: any[] = [];
268
- const provider = createMockProvider((...args: any[]) => {
269
- callCount++;
270
- callArgs.push(args);
271
- if (callCount === 1) return analysisResponse();
272
- if (callCount === 2) return codeGenResponse();
273
- if (callCount === 3) return qaRejectResponse('Bug', ['Fix it']);
274
- if (callCount === 4) return codeGenResponse(); // iter 2 code-gen
275
- return qaApproveResponse();
276
- });
277
-
278
- const engine = createEngine(provider);
279
- await engine.run({ pipeline: 'group-test', input: 'Build feature' });
280
-
281
- // The 4th call is code-gen iteration 2 — it should have analysis output in context
282
- // (Verified via provider.call args which include messages containing the context)
283
- expect(provider.call).toHaveBeenCalledTimes(5);
284
- });
285
-
286
- it('passes object issues from QA rejection into group_feedback for the coder', async () => {
287
- // Regression test for STU-129: group_feedback.rejection_details was empty when
288
- // QA returned issues as objects with field names other than "description".
289
- let callCount = 0;
290
- let coderIter2Messages: any[] = [];
291
- const provider = createMockProvider((...args: any[]) => {
292
- callCount++;
293
- if (callCount === 1) return analysisResponse();
294
- if (callCount === 2) return codeGenResponse(); // iter 1
295
- if (callCount === 3) {
296
- // QA rejects with structured issues (objects with "issue" field, not "description")
297
- return {
298
- content: JSON.stringify({
299
- status: 'needs_changes',
300
- summary: 'Several problems found',
301
- issues: [
302
- { issue: 'Missing error handling', severity: 'high' },
303
- { issue: 'No input validation', file: 'src/api.ts' },
304
- ],
305
- }),
306
- tool_calls: [],
307
- finish_reason: 'stop',
308
- usage: { prompt_tokens: 150, completion_tokens: 50, total_tokens: 200 },
309
- };
310
- }
311
- if (callCount === 4) {
312
- // Capture the messages sent to the coder on iteration 2
313
- coderIter2Messages = args[0]?.messages ?? [];
314
- return codeGenResponse();
315
- }
316
- return qaApproveResponse();
317
- });
318
-
319
- const engine = createEngine(provider);
320
- const result = await engine.run({ pipeline: 'group-test', input: 'Build feature' });
321
-
322
- expect(result.status).toBe('success');
323
- expect(provider.call).toHaveBeenCalledTimes(5);
324
-
325
- // The coder on iteration 2 must have received group_feedback with the specific issues
326
- const userMessage = coderIter2Messages.find((m: any) => m.role === 'user');
327
- expect(userMessage).toBeDefined();
328
- expect(userMessage.content).toContain('REVISION REQUIRED');
329
- expect(userMessage.content).toContain('Missing error handling');
330
- expect(userMessage.content).toContain('No input validation');
331
- });
332
-
333
- it('fails pipeline (not group retry) when executor throws during group iteration', async () => {
334
- let callCount = 0;
335
- const provider = createMockProvider(() => {
336
- callCount++;
337
- if (callCount === 1) return analysisResponse(); // pre-group analysis
338
- if (callCount === 2) return codeGenResponse(); // iter 1: code-gen OK
339
- if (callCount === 3) return qaRejectResponse( // iter 1: QA rejects
340
- 'Missing error handling',
341
- ['No try-catch around API call']
342
- );
343
- // iter 2: code-gen throws a technical error (network timeout, etc.)
344
- throw new Error('Network timeout');
345
- });
346
-
347
- const engine = createEngine(provider);
348
- const result = await engine.run({ pipeline: 'group-test', input: 'Build feature' });
349
-
350
- // Technical throw → stage failed → pipeline failed, no infinite group retry
351
- expect(result.status).toBe('failed');
352
- // analysis + code-gen(iter1) + qa-reject(iter1) + code-gen(iter2 throws) = 4
353
- expect(result.stages).toHaveLength(4);
354
- expect(provider.call).toHaveBeenCalledTimes(4);
355
- });
356
-
357
- it('emits group lifecycle events', async () => {
358
- let callCount = 0;
359
- const provider = createMockProvider(() => {
360
- callCount++;
361
- if (callCount === 1) return analysisResponse();
362
- if (callCount === 2) return codeGenResponse();
363
- if (callCount === 3) return qaRejectResponse('Bug', ['Fix it']);
364
- if (callCount === 4) return codeGenResponse();
365
- return qaApproveResponse();
366
- });
367
-
368
- const events: string[] = [];
369
- const engineEvents: EngineEvents = {
370
- onGroupStart: () => events.push('group_start'),
371
- onGroupIteration: (e) => events.push(`group_iter_${e.iteration}`),
372
- onGroupFeedback: () => events.push('group_feedback'),
373
- onGroupComplete: (e) => events.push(`group_complete_${e.status}`),
374
- onStageStart: () => {},
375
- onStageComplete: () => {},
376
- onPipelineStart: () => {},
377
- onPipelineComplete: () => {},
378
- };
379
-
380
- const engine = createEngine(provider, engineEvents);
381
- await engine.run({ pipeline: 'group-test', input: 'Build feature' });
382
-
383
- expect(events).toContain('group_start');
384
- expect(events).toContain('group_iter_1');
385
- expect(events).toContain('group_feedback');
386
- expect(events).toContain('group_iter_2');
387
- expect(events).toContain('group_complete_success');
388
- });
389
- });