@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,186 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { resolve } from 'node:path';
3
- import { PipelineEngine } from '../engine.js';
4
- import type { PipelineDefinition } from '@studio-foundation/contracts';
5
-
6
- // Mock the runner so we can inspect which stages actually ran
7
- vi.mock('@studio-foundation/runner', async (importOriginal) => {
8
- const actual = await importOriginal<typeof import('@studio-foundation/runner')>();
9
- return {
10
- ...actual,
11
- runScript: vi.fn(),
12
- };
13
- });
14
-
15
- import { runScript } from '@studio-foundation/runner';
16
-
17
- const FIXTURES_DIR = resolve(__dirname, '__fixtures__/script-stage');
18
-
19
- function makeEngine() {
20
- return new PipelineEngine({
21
- configsDir: FIXTURES_DIR,
22
- providerRegistry: {} as any,
23
- });
24
- }
25
-
26
- function mockScriptSuccess(output: Record<string, unknown> = { result: 'ok' }) {
27
- vi.mocked(runScript).mockResolvedValue({
28
- output,
29
- tool_calls: [],
30
- tool_calls_count: 0,
31
- duration_ms: 10,
32
- });
33
- }
34
-
35
- describe('engine — stage conditions', () => {
36
- beforeEach(() => {
37
- vi.clearAllMocks();
38
- });
39
-
40
- it('skips a stage when input condition is false', async () => {
41
- mockScriptSuccess();
42
-
43
- const pipeline: PipelineDefinition = {
44
- name: 'test-conditions',
45
- description: 'test',
46
- version: 1,
47
- stages: [
48
- {
49
- name: 'always-runs',
50
- executor: 'script',
51
- script: 'scripts/parse.py',
52
- runtime: 'shell',
53
- },
54
- {
55
- name: 'conditional-stage',
56
- executor: 'script',
57
- script: 'scripts/parse.py',
58
- runtime: 'shell',
59
- condition: 'input.meals_count >= 6',
60
- },
61
- ],
62
- };
63
-
64
- const engine = makeEngine();
65
- const result = await engine.run({
66
- pipelineDef: pipeline,
67
- input: { meals_count: 3 }, // condition is false — stage should skip
68
- });
69
-
70
- expect(result.status).toBe('success');
71
- expect(result.stages).toHaveLength(2);
72
- expect(result.stages[0]?.status).toBe('success');
73
- expect(result.stages[1]?.status).toBe('skipped');
74
- // Script was only called once (for always-runs, not conditional-stage)
75
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(1);
76
- });
77
-
78
- it('runs a stage when input condition is true', async () => {
79
- mockScriptSuccess();
80
-
81
- const pipeline: PipelineDefinition = {
82
- name: 'test-conditions-true',
83
- description: 'test',
84
- version: 1,
85
- stages: [
86
- {
87
- name: 'conditional-stage',
88
- executor: 'script',
89
- script: 'scripts/parse.py',
90
- runtime: 'shell',
91
- condition: 'input.meals_count >= 6',
92
- },
93
- ],
94
- };
95
-
96
- const engine = makeEngine();
97
- const result = await engine.run({
98
- pipelineDef: pipeline,
99
- input: { meals_count: 7 }, // condition is true — stage should run
100
- });
101
-
102
- expect(result.status).toBe('success');
103
- expect(result.stages[0]?.status).toBe('success');
104
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(1);
105
- });
106
-
107
- it('skips a stage based on previous stage output', async () => {
108
- // First call returns extraction result, second call would be entity-resolution
109
- vi.mocked(runScript).mockResolvedValueOnce({
110
- output: { counts: { OTHER: 0, PERSON: 2 } },
111
- tool_calls: [],
112
- tool_calls_count: 0,
113
- duration_ms: 10,
114
- });
115
-
116
- const pipeline: PipelineDefinition = {
117
- name: 'test-conditions-stage-output',
118
- description: 'test',
119
- version: 1,
120
- stages: [
121
- {
122
- name: 'entity-extraction',
123
- executor: 'script',
124
- script: 'scripts/extract.py',
125
- runtime: 'shell',
126
- context: { include: ['input'] },
127
- },
128
- {
129
- name: 'entity-resolution-OTHER',
130
- executor: 'script',
131
- script: 'scripts/resolve.py',
132
- runtime: 'shell',
133
- condition: 'stages.entity-extraction.output.counts.OTHER > 0',
134
- context: { include: ['all_stage_outputs'] },
135
- },
136
- ],
137
- };
138
-
139
- const engine = makeEngine();
140
- const result = await engine.run({
141
- pipelineDef: pipeline,
142
- input: 'extract entities',
143
- });
144
-
145
- expect(result.status).toBe('success');
146
- expect(result.stages[0]?.status).toBe('success');
147
- expect(result.stages[1]?.status).toBe('skipped'); // counts.OTHER is 0
148
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(1); // only extraction ran
149
- });
150
-
151
- it('pipeline continues after skipped stage', async () => {
152
- mockScriptSuccess({ final: 'result' });
153
-
154
- const pipeline: PipelineDefinition = {
155
- name: 'test-skip-continues',
156
- description: 'test',
157
- version: 1,
158
- stages: [
159
- {
160
- name: 'skipped-stage',
161
- executor: 'script',
162
- script: 'scripts/parse.py',
163
- runtime: 'shell',
164
- condition: 'input.run_optional >= 1',
165
- },
166
- {
167
- name: 'final-stage',
168
- executor: 'script',
169
- script: 'scripts/finalize.py',
170
- runtime: 'shell',
171
- },
172
- ],
173
- };
174
-
175
- const engine = makeEngine();
176
- const result = await engine.run({
177
- pipelineDef: pipeline,
178
- input: { run_optional: 0 },
179
- });
180
-
181
- expect(result.status).toBe('success');
182
- expect(result.stages[0]?.status).toBe('skipped');
183
- expect(result.stages[1]?.status).toBe('success');
184
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(1); // only final-stage
185
- });
186
- });
@@ -1,108 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { PipelineEngine } from '../engine.js';
3
- import type { PipelineDefinition } from '@studio-foundation/contracts';
4
-
5
- vi.mock('@studio-foundation/runner', async (importOriginal) => {
6
- const actual = await importOriginal<typeof import('@studio-foundation/runner')>();
7
- return {
8
- ...actual,
9
- runScript: vi.fn(),
10
- };
11
- });
12
-
13
- import { runScript } from '@studio-foundation/runner';
14
-
15
- function makeEngine() {
16
- return new PipelineEngine({
17
- configsDir: '/tmp',
18
- providerRegistry: {} as any,
19
- });
20
- }
21
-
22
- function mockScriptSuccess(output: Record<string, unknown> = { result: 'ok' }) {
23
- vi.mocked(runScript).mockResolvedValue({
24
- output,
25
- tool_calls: [],
26
- tool_calls_count: 0,
27
- duration_ms: 10,
28
- });
29
- }
30
-
31
- describe('engine — resume from stage', () => {
32
- beforeEach(() => vi.clearAllMocks());
33
-
34
- const pipeline: PipelineDefinition = {
35
- name: 'test-resume',
36
- description: 'test',
37
- version: 1,
38
- stages: [
39
- { name: 'stage-a', executor: 'script', script: 'x.py', runtime: 'shell' },
40
- { name: 'stage-b', executor: 'script', script: 'x.py', runtime: 'shell' },
41
- { name: 'stage-c', executor: 'script', script: 'x.py', runtime: 'shell' },
42
- ],
43
- };
44
-
45
- it('skips stages before resumeFromStage and marks them skipped', async () => {
46
- mockScriptSuccess({ result: 'c-result' });
47
-
48
- const engine = makeEngine();
49
- const result = await engine.run({
50
- pipelineDef: pipeline,
51
- input: { x: 1 },
52
- resumeFromStage: 'stage-c',
53
- priorStageOutputs: new Map([
54
- ['stage-a', { result: 'a-cached' }],
55
- ['stage-b', { result: 'b-cached' }],
56
- ]),
57
- originalRunId: 'abc12345',
58
- });
59
-
60
- expect(result.status).toBe('success');
61
- expect(result.stages).toHaveLength(3);
62
- expect(result.stages[0]?.stage_name).toBe('stage-a');
63
- expect(result.stages[0]?.status).toBe('skipped');
64
- expect(result.stages[0]?.skipped_reason).toContain('abc12345');
65
- expect(result.stages[1]?.stage_name).toBe('stage-b');
66
- expect(result.stages[1]?.status).toBe('skipped');
67
- expect(result.stages[2]?.stage_name).toBe('stage-c');
68
- expect(result.stages[2]?.status).toBe('success');
69
- // Only stage-c was actually executed
70
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(1);
71
- });
72
-
73
- it('pre-populates context so resumed stage can access prior outputs', async () => {
74
- mockScriptSuccess({ result: 'ok' });
75
-
76
- const engine = makeEngine();
77
- const result = await engine.run({
78
- pipelineDef: {
79
- ...pipeline,
80
- stages: [
81
- { name: 'stage-a', executor: 'script', script: 'x.py', runtime: 'shell' },
82
- { name: 'stage-b', executor: 'script', script: 'x.py', runtime: 'shell' },
83
- ],
84
- },
85
- input: { x: 1 },
86
- resumeFromStage: 'stage-b',
87
- priorStageOutputs: new Map([['stage-a', { result: 'a-cached' }]]),
88
- });
89
-
90
- // stage-b ran successfully (prior context was pre-populated, no crash)
91
- expect(result.status).toBe('success');
92
- expect(result.stages).toHaveLength(2);
93
- expect(result.stages[0]?.status).toBe('skipped');
94
- expect(result.stages[1]?.status).toBe('success');
95
- });
96
-
97
- it('throws if resumeFromStage is not found in pipeline', async () => {
98
- const engine = makeEngine();
99
- await expect(
100
- engine.run({
101
- pipelineDef: pipeline,
102
- input: {},
103
- resumeFromStage: 'nonexistent-stage',
104
- priorStageOutputs: new Map(),
105
- })
106
- ).rejects.toThrow(/not found/i);
107
- });
108
- });
@@ -1,125 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { resolve } from 'node:path';
3
- import { PipelineEngine } from '../engine.js';
4
- import type { PipelineDefinition } from '@studio-foundation/contracts';
5
-
6
- // Mock runScript from runner to avoid real subprocess spawning
7
- vi.mock('@studio-foundation/runner', async (importOriginal) => {
8
- const actual = await importOriginal<typeof import('@studio-foundation/runner')>();
9
- return {
10
- ...actual,
11
- runScript: vi.fn(),
12
- };
13
- });
14
-
15
- import { runScript } from '@studio-foundation/runner';
16
-
17
- const FIXTURES_DIR = resolve(__dirname, '__fixtures__/script-stage');
18
-
19
- const SCRIPT_PIPELINE: PipelineDefinition = {
20
- name: 'test-script-pipeline',
21
- description: 'Test pipeline with script stage',
22
- version: 1,
23
- stages: [
24
- {
25
- name: 'epub-ingestion',
26
- executor: 'script',
27
- script: 'scripts/parse.py',
28
- runtime: 'python',
29
- contract: 'book-context',
30
- },
31
- ],
32
- };
33
-
34
- function makeEngine() {
35
- return new PipelineEngine({
36
- configsDir: FIXTURES_DIR,
37
- providerRegistry: {} as any,
38
- });
39
- }
40
-
41
- describe('engine — script stage execution', () => {
42
- beforeEach(() => {
43
- vi.clearAllMocks();
44
- });
45
-
46
- it('executes a script stage and returns success when output matches contract', async () => {
47
- vi.mocked(runScript).mockResolvedValue({
48
- output: { title: 'My Book', chapters: 3 },
49
- tool_calls: [],
50
- tool_calls_count: 0,
51
- duration_ms: 50,
52
- });
53
-
54
- const engine = makeEngine();
55
- const result = await engine.run({
56
- pipelineDef: SCRIPT_PIPELINE,
57
- userInput: 'parse book.epub',
58
- });
59
-
60
- expect(result.status).toBe('success');
61
- expect(result.stages[0]?.status).toBe('success');
62
- expect(result.stages[0]?.output).toEqual({ title: 'My Book', chapters: 3 });
63
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(1);
64
- expect(vi.mocked(runScript)).toHaveBeenCalledWith(
65
- expect.objectContaining({
66
- scriptPath: 'scripts/parse.py',
67
- runtime: 'python',
68
- }),
69
- );
70
- });
71
-
72
- it('retries on script error (runScript returns error field)', async () => {
73
- const scriptError = {
74
- output: null,
75
- tool_calls: [],
76
- tool_calls_count: 0,
77
- duration_ms: 10,
78
- error: 'Script exited with code 1: parse error',
79
- };
80
- vi.mocked(runScript)
81
- .mockResolvedValueOnce(scriptError)
82
- .mockResolvedValueOnce(scriptError)
83
- .mockResolvedValue({
84
- output: { title: 'My Book', chapters: 3 },
85
- tool_calls: [],
86
- tool_calls_count: 0,
87
- duration_ms: 50,
88
- });
89
-
90
- const engine = makeEngine();
91
- const result = await engine.run({
92
- pipelineDef: {
93
- ...SCRIPT_PIPELINE,
94
- stages: [{ ...SCRIPT_PIPELINE.stages[0], ralph: { max_attempts: 3, retry_strategy: 'none' } }],
95
- },
96
- userInput: 'parse book.epub',
97
- });
98
-
99
- expect(result.status).toBe('success');
100
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(3);
101
- });
102
-
103
- it('fails stage after exhausting max_attempts', async () => {
104
- vi.mocked(runScript).mockResolvedValue({
105
- output: null,
106
- tool_calls: [],
107
- tool_calls_count: 0,
108
- duration_ms: 10,
109
- error: 'Script exited with code 1: always failing',
110
- });
111
-
112
- const engine = makeEngine();
113
- const result = await engine.run({
114
- pipelineDef: {
115
- ...SCRIPT_PIPELINE,
116
- stages: [{ ...SCRIPT_PIPELINE.stages[0], ralph: { max_attempts: 2, retry_strategy: 'none' } }],
117
- },
118
- userInput: 'parse book.epub',
119
- });
120
-
121
- expect(result.status).toBe('failed');
122
- expect(result.stages[0]?.status).toBe('failed');
123
- expect(vi.mocked(runScript)).toHaveBeenCalledTimes(2);
124
- });
125
- });
@@ -1,175 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { PipelineEngine } from './engine.js';
6
- import { createDefaultRegistry, ToolRegistry, MockProvider } from '@studio-foundation/runner';
7
- import type { StageContextEvent } from './events.js';
8
-
9
- async function makeTestDirs(): Promise<{ configsDir: string }> {
10
- const base = await mkdtemp(join(tmpdir(), 'studio-ctx-test-'));
11
- const configsDir = join(base, '.studio');
12
- await mkdir(join(configsDir, 'pipelines'), { recursive: true });
13
- await mkdir(join(configsDir, 'agents'), { recursive: true });
14
- await mkdir(join(configsDir, 'contracts'), { recursive: true });
15
-
16
- await writeFile(
17
- join(configsDir, 'pipelines', 'test-pipe.pipeline.yaml'),
18
- `
19
- name: test-pipe
20
- description: test
21
- version: 1
22
- stages:
23
- - name: my-stage
24
- kind: analysis
25
- agent: analyst
26
- contract: stage-contract
27
- context:
28
- include: [input]
29
- `
30
- );
31
-
32
- await writeFile(
33
- join(configsDir, 'agents', 'analyst.agent.yaml'),
34
- `
35
- name: analyst
36
- provider: mock
37
- model: mock
38
- system_prompt: "You are an analyst."
39
- `
40
- );
41
-
42
- await writeFile(
43
- join(configsDir, 'contracts', 'stage-contract.contract.yaml'),
44
- `
45
- name: stage-contract
46
- version: 1
47
- schema:
48
- required_fields:
49
- - summary
50
- `
51
- );
52
-
53
- return { configsDir };
54
- }
55
-
56
- // MockProvider keys on contract_name (= stage_contract). Use this in all tests.
57
- const MOCK_STAGE_KEY = 'stage-contract';
58
-
59
- describe('PipelineEngine — onStageContext event', () => {
60
- it('emits onStageContext once per stage', async () => {
61
- const { configsDir } = await makeTestDirs();
62
-
63
- const mockStages = new Map([
64
- [MOCK_STAGE_KEY, { output: { summary: 'done' }, tool_calls: [] }],
65
- ]);
66
- const mockProvider = new MockProvider(mockStages);
67
- const providerRegistry = createDefaultRegistry({});
68
- providerRegistry.register(mockProvider);
69
- const toolRegistry = new ToolRegistry();
70
-
71
- const received: StageContextEvent[] = [];
72
-
73
- const engine = new PipelineEngine(
74
- { configsDir, providerRegistry, toolRegistry },
75
- { onStageContext: (e) => received.push(e) }
76
- );
77
-
78
- await engine.run({ pipeline: 'test-pipe', input: 'test input' });
79
-
80
- expect(received).toHaveLength(1);
81
- expect(received[0].stage).toBe('my-stage');
82
- expect(received[0].run_id).toMatch(/^[0-9a-f-]{36}$/); // uuid
83
- expect(received[0].context_keys.input).toBe('test input'.length);
84
- });
85
-
86
- it('includes no context_content by default (DEBUG unset)', async () => {
87
- const { configsDir } = await makeTestDirs();
88
- delete process.env.DEBUG;
89
-
90
- const mockProvider = new MockProvider(
91
- new Map([[MOCK_STAGE_KEY, { output: { summary: 'done' }, tool_calls: [] }]])
92
- );
93
- const providerRegistry = createDefaultRegistry({});
94
- providerRegistry.register(mockProvider);
95
-
96
- const received: StageContextEvent[] = [];
97
- const engine = new PipelineEngine(
98
- { configsDir, providerRegistry, toolRegistry: new ToolRegistry() },
99
- { onStageContext: (e) => received.push(e) }
100
- );
101
-
102
- await engine.run({ pipeline: 'test-pipe', input: 'hello' });
103
-
104
- expect(received[0].context_content).toBeUndefined();
105
- expect(received[0].system_prompt).toBeUndefined();
106
- });
107
-
108
- it('includes context_content when DEBUG=studio:context', async () => {
109
- const { configsDir } = await makeTestDirs();
110
- process.env.DEBUG = 'studio:context';
111
-
112
- const mockProvider = new MockProvider(
113
- new Map([[MOCK_STAGE_KEY, { output: { summary: 'done' }, tool_calls: [] }]])
114
- );
115
- const providerRegistry = createDefaultRegistry({});
116
- providerRegistry.register(mockProvider);
117
-
118
- const received: StageContextEvent[] = [];
119
- const engine = new PipelineEngine(
120
- { configsDir, providerRegistry, toolRegistry: new ToolRegistry() },
121
- { onStageContext: (e) => received.push(e) }
122
- );
123
-
124
- await engine.run({ pipeline: 'test-pipe', input: 'hello' });
125
-
126
- expect(received[0].context_content).toBeDefined();
127
- expect(received[0].context_content?.input).toBe('hello');
128
- expect(received[0].system_prompt).toBeUndefined();
129
-
130
- delete process.env.DEBUG;
131
- });
132
-
133
- it('includes system_prompt when DEBUG=studio:context:verbose', async () => {
134
- const { configsDir } = await makeTestDirs();
135
- process.env.DEBUG = 'studio:context:verbose';
136
-
137
- const mockProvider = new MockProvider(
138
- new Map([[MOCK_STAGE_KEY, { output: { summary: 'done' }, tool_calls: [] }]])
139
- );
140
- const providerRegistry = createDefaultRegistry({});
141
- providerRegistry.register(mockProvider);
142
-
143
- const received: StageContextEvent[] = [];
144
- const engine = new PipelineEngine(
145
- { configsDir, providerRegistry, toolRegistry: new ToolRegistry() },
146
- { onStageContext: (e) => received.push(e) }
147
- );
148
-
149
- await engine.run({ pipeline: 'test-pipe', input: 'hello' });
150
-
151
- expect(received[0].context_content).toBeDefined(); // verbose implies context
152
- expect(received[0].system_prompt).toBe('You are an analyst.');
153
-
154
- delete process.env.DEBUG;
155
- });
156
-
157
- it('does not call handler at all when onStageContext is not registered', async () => {
158
- const { configsDir } = await makeTestDirs();
159
-
160
- const mockProvider = new MockProvider(
161
- new Map([[MOCK_STAGE_KEY, { output: { summary: 'done' }, tool_calls: [] }]])
162
- );
163
- const providerRegistry = createDefaultRegistry({});
164
- providerRegistry.register(mockProvider);
165
-
166
- // No onStageContext handler
167
- const engine = new PipelineEngine(
168
- { configsDir, providerRegistry, toolRegistry: new ToolRegistry() },
169
- { onStageComplete: vi.fn() } // some other handler but not onStageContext
170
- );
171
-
172
- // Should not throw
173
- await engine.run({ pipeline: 'test-pipe', input: 'hello' });
174
- });
175
- });