edsger 0.44.0 → 0.45.1

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 (105) hide show
  1. package/dist/api/run-sheets.d.ts +22 -0
  2. package/dist/api/run-sheets.js +13 -0
  3. package/dist/commands/run-sheet/index.d.ts +6 -0
  4. package/dist/commands/run-sheet/index.js +48 -0
  5. package/dist/index.js +22 -0
  6. package/dist/phases/run-sheet/index.d.ts +39 -0
  7. package/dist/phases/run-sheet/index.js +297 -0
  8. package/dist/phases/run-sheet/render.d.ts +42 -0
  9. package/dist/phases/run-sheet/render.js +133 -0
  10. package/package.json +11 -4
  11. package/tsconfig.build.json +4 -0
  12. package/tsconfig.json +3 -8
  13. package/vitest.config.ts +12 -0
  14. package/dist/api/__tests__/app-store.test.d.ts +0 -7
  15. package/dist/api/__tests__/app-store.test.js +0 -60
  16. package/dist/api/__tests__/intelligence.test.d.ts +0 -11
  17. package/dist/api/__tests__/intelligence.test.js +0 -315
  18. package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
  19. package/dist/api/features/__tests__/feature-utils.test.js +0 -370
  20. package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
  21. package/dist/api/features/__tests__/status-updater.test.js +0 -88
  22. package/dist/commands/build/__tests__/build.test.d.ts +0 -5
  23. package/dist/commands/build/__tests__/build.test.js +0 -206
  24. package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
  25. package/dist/commands/build/__tests__/detect-project.test.js +0 -160
  26. package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
  27. package/dist/commands/build/__tests__/run-build.test.js +0 -433
  28. package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
  29. package/dist/commands/intelligence/__tests__/command.test.js +0 -48
  30. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
  31. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
  32. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
  33. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
  34. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
  35. package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
  36. package/dist/config/__tests__/config.test.d.ts +0 -4
  37. package/dist/config/__tests__/config.test.js +0 -286
  38. package/dist/config/__tests__/feature-status.test.d.ts +0 -4
  39. package/dist/config/__tests__/feature-status.test.js +0 -111
  40. package/dist/errors/__tests__/index.test.d.ts +0 -4
  41. package/dist/errors/__tests__/index.test.js +0 -349
  42. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
  43. package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
  44. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
  45. package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
  46. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
  47. package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
  48. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
  49. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
  50. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
  51. package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
  52. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
  53. package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
  54. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
  55. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
  56. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
  57. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
  58. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
  59. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
  60. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
  61. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
  62. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
  63. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
  64. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
  65. package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
  66. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
  67. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
  68. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
  69. package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
  70. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
  71. package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
  72. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
  73. package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
  74. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
  75. package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
  76. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
  77. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
  78. package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
  79. package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
  80. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
  81. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
  82. package/dist/phases/release-sync/__tests__/github.test.d.ts +0 -9
  83. package/dist/phases/release-sync/__tests__/github.test.js +0 -123
  84. package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +0 -8
  85. package/dist/phases/release-sync/__tests__/snapshot.test.js +0 -93
  86. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
  87. package/dist/phases/smoke-test/__tests__/agent.test.js +0 -85
  88. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
  89. package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
  90. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
  91. package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
  92. package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
  93. package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
  94. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
  95. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
  96. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
  97. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
  98. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
  99. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
  100. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
  101. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
  102. package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
  103. package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
  104. package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
  105. package/dist/workspace/__tests__/workspace-manager.test.js +0 -52
@@ -1,321 +0,0 @@
1
- import assert from 'node:assert';
2
- import { describe, it } from 'node:test';
3
- import { buildHookPrompt, executeHook, parseHookResponse, } from '../hook-executor.js';
4
- function makeBinding(overrides = {}) {
5
- return {
6
- id: 'hook-1',
7
- product_id: 'prod-1',
8
- phase: 'technical-design',
9
- hook_point: 'after',
10
- plugin_name: 'payload-cms',
11
- skill_name: 'validate-schema',
12
- on_failure: 'block',
13
- config: {},
14
- sort_order: 0,
15
- enabled: true,
16
- ...overrides,
17
- };
18
- }
19
- const defaultSkillFile = {
20
- frontmatter: { model: 'sonnet', maxTurns: 10 },
21
- body: 'You are a schema validator.',
22
- };
23
- function makeContext(overrides = {}) {
24
- return {
25
- featureId: 'feat-1',
26
- phase: 'technical-design',
27
- hookPoint: 'after',
28
- ...overrides,
29
- };
30
- }
31
- // ---- buildHookPrompt ----
32
- void describe('buildHookPrompt', () => {
33
- void it('includes feature ID and phase name', () => {
34
- const ctx = {
35
- featureId: 'feat-123',
36
- phase: 'technical-design',
37
- hookPoint: 'after',
38
- };
39
- const prompt = buildHookPrompt(makeBinding(), ctx);
40
- assert.ok(prompt.includes('feat-123'));
41
- assert.ok(prompt.includes('technical-design'));
42
- });
43
- void it('includes plugin and skill names', () => {
44
- const ctx = {
45
- featureId: 'feat-1',
46
- phase: 'code-review',
47
- hookPoint: 'before',
48
- };
49
- const prompt = buildHookPrompt(makeBinding(), ctx);
50
- assert.ok(prompt.includes('payload-cms'));
51
- assert.ok(prompt.includes('validate-schema'));
52
- });
53
- void it('includes hook point', () => {
54
- const ctx = {
55
- featureId: 'feat-1',
56
- phase: 'code-review',
57
- hookPoint: 'before',
58
- };
59
- const prompt = buildHookPrompt(makeBinding(), ctx);
60
- assert.ok(prompt.includes('before'));
61
- });
62
- void it('includes phase result for after hooks', () => {
63
- const ctx = {
64
- featureId: 'feat-1',
65
- phase: 'technical-design',
66
- hookPoint: 'after',
67
- phaseResult: { status: 'success', iterations: 3 },
68
- };
69
- const prompt = buildHookPrompt(makeBinding(), ctx);
70
- assert.ok(prompt.includes('Phase Result'));
71
- assert.ok(prompt.includes('"iterations": 3'));
72
- });
73
- void it('does not include phase result for before hooks', () => {
74
- const ctx = {
75
- featureId: 'feat-1',
76
- phase: 'technical-design',
77
- hookPoint: 'before',
78
- };
79
- const prompt = buildHookPrompt(makeBinding(), ctx);
80
- assert.ok(!prompt.includes('Phase Result'));
81
- });
82
- void it('includes error for on_error hooks', () => {
83
- const ctx = {
84
- featureId: 'feat-1',
85
- phase: 'code-implementation',
86
- hookPoint: 'on_error',
87
- error: new Error('Out of memory'),
88
- };
89
- const prompt = buildHookPrompt(makeBinding(), ctx);
90
- assert.ok(prompt.includes('Phase Error'));
91
- assert.ok(prompt.includes('Out of memory'));
92
- });
93
- void it('includes binding config when present', () => {
94
- const binding = makeBinding({
95
- config: { severity: 'high', threshold: 0.8 },
96
- });
97
- const ctx = {
98
- featureId: 'feat-1',
99
- phase: 'technical-design',
100
- hookPoint: 'after',
101
- };
102
- const prompt = buildHookPrompt(binding, ctx);
103
- assert.ok(prompt.includes('Hook Configuration'));
104
- assert.ok(prompt.includes('"severity": "high"'));
105
- });
106
- void it('omits config section when config is empty', () => {
107
- const ctx = {
108
- featureId: 'feat-1',
109
- phase: 'technical-design',
110
- hookPoint: 'after',
111
- };
112
- const prompt = buildHookPrompt(makeBinding(), ctx);
113
- assert.ok(!prompt.includes('Hook Configuration'));
114
- });
115
- });
116
- // ---- parseHookResponse ----
117
- void describe('parseHookResponse', () => {
118
- void it('parses JSON in fenced code block', () => {
119
- const text = 'Some text\n```json\n{"status":"success","message":"All good","data":{"count":5}}\n```\nMore text';
120
- const result = parseHookResponse(text);
121
- assert.strictEqual(result.status, 'success');
122
- assert.strictEqual(result.message, 'All good');
123
- assert.deepStrictEqual(result.data, { count: 5 });
124
- });
125
- void it('parses raw JSON text', () => {
126
- const text = '{"status":"error","message":"Schema mismatch"}';
127
- const result = parseHookResponse(text);
128
- assert.strictEqual(result.status, 'error');
129
- assert.strictEqual(result.message, 'Schema mismatch');
130
- });
131
- void it('treats non-JSON text as success', () => {
132
- const text = 'Everything looks fine, no issues found.';
133
- const result = parseHookResponse(text);
134
- assert.strictEqual(result.status, 'success');
135
- assert.ok(result.message.includes('Everything looks fine'));
136
- });
137
- void it('truncates long non-JSON text', () => {
138
- const text = 'x'.repeat(1000);
139
- const result = parseHookResponse(text);
140
- assert.strictEqual(result.message.length, 500);
141
- });
142
- void it('handles empty text', () => {
143
- const result = parseHookResponse('');
144
- assert.strictEqual(result.status, 'success');
145
- assert.ok(result.message.includes('no structured output'));
146
- });
147
- void it('handles malformed JSON in code block gracefully', () => {
148
- const text = '```json\n{invalid json}\n```';
149
- const result = parseHookResponse(text);
150
- assert.strictEqual(result.status, 'success');
151
- });
152
- void it('defaults message when JSON has no message field', () => {
153
- const text = '{"status":"success"}';
154
- const result = parseHookResponse(text);
155
- assert.strictEqual(result.message, 'Hook completed');
156
- });
157
- });
158
- // ---- executeHook (with injected deps) ----
159
- void describe('executeHook', () => {
160
- /** Create a mock queryFn that yields the given messages */
161
- function mockQuery(messages
162
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
- ) {
164
- // Return a function that returns an async iterable
165
- return () => ({
166
- // eslint-disable-next-line @typescript-eslint/require-await
167
- async *[Symbol.asyncIterator]() {
168
- for (const msg of messages) {
169
- yield msg;
170
- }
171
- },
172
- });
173
- }
174
- function makeDeps(overrides = {}) {
175
- return {
176
- loadSkillFile: () => Promise.resolve(defaultSkillFile),
177
- queryFn: mockQuery([
178
- {
179
- type: 'result',
180
- subtype: 'success',
181
- result: '```json\n{"status":"success","message":"Schema valid"}\n```',
182
- },
183
- ]),
184
- ...overrides,
185
- };
186
- }
187
- void it('returns skipped when skill file not found', async () => {
188
- const deps = makeDeps({ loadSkillFile: () => Promise.resolve(null) });
189
- const result = await executeHook(makeBinding(), makeContext(), false, deps);
190
- assert.strictEqual(result.status, 'skipped');
191
- assert.ok(result.message.includes('not found'));
192
- });
193
- void it('executes successfully with result message from query', async () => {
194
- const deps = makeDeps();
195
- const result = await executeHook(makeBinding(), makeContext(), false, deps);
196
- assert.strictEqual(result.status, 'success');
197
- assert.strictEqual(result.message, 'Schema valid');
198
- assert.ok(result.duration_ms >= 0);
199
- assert.strictEqual(result.hookId, 'hook-1');
200
- });
201
- void it('returns error status when skill returns error JSON', async () => {
202
- const deps = makeDeps({
203
- queryFn: mockQuery([
204
- {
205
- type: 'result',
206
- subtype: 'success',
207
- result: '{"status":"error","message":"3 issues found"}',
208
- },
209
- ]),
210
- });
211
- const result = await executeHook(makeBinding(), makeContext(), false, deps);
212
- assert.strictEqual(result.status, 'error');
213
- assert.strictEqual(result.message, '3 issues found');
214
- });
215
- void it('collects text from assistant messages', async () => {
216
- const deps = makeDeps({
217
- queryFn: mockQuery([
218
- {
219
- type: 'assistant',
220
- message: {
221
- content: [{ type: 'text', text: '```json\n{"status":"success",' }],
222
- },
223
- },
224
- {
225
- type: 'assistant',
226
- message: {
227
- content: [
228
- { type: 'text', text: '"message":"from streaming"}\n```' },
229
- ],
230
- },
231
- },
232
- { type: 'result', subtype: 'success', result: '' },
233
- ]),
234
- });
235
- const result = await executeHook(makeBinding(), makeContext(), false, deps);
236
- assert.strictEqual(result.status, 'success');
237
- assert.strictEqual(result.message, 'from streaming');
238
- });
239
- void it('prefers result message over streamed text', async () => {
240
- const deps = makeDeps({
241
- queryFn: mockQuery([
242
- {
243
- type: 'assistant',
244
- message: { content: [{ type: 'text', text: 'streamed partial' }] },
245
- },
246
- {
247
- type: 'result',
248
- subtype: 'success',
249
- result: '{"status":"success","message":"final result"}',
250
- },
251
- ]),
252
- });
253
- const result = await executeHook(makeBinding(), makeContext(), false, deps);
254
- assert.strictEqual(result.message, 'final result');
255
- });
256
- void it('returns error when query throws', async () => {
257
- const deps = makeDeps({
258
- queryFn: (() => {
259
- throw new Error('Network timeout');
260
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
261
- }),
262
- });
263
- const result = await executeHook(makeBinding(), makeContext(), false, deps);
264
- assert.strictEqual(result.status, 'error');
265
- assert.ok(result.message.includes('Network timeout'));
266
- });
267
- void it('passes correct model and maxTurns from frontmatter', async () => {
268
- let capturedOptions = {};
269
- const deps = makeDeps({
270
- loadSkillFile: () => Promise.resolve({
271
- frontmatter: { model: 'haiku', maxTurns: 5 },
272
- body: 'Test prompt',
273
- }),
274
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
- queryFn: ((opts) => {
276
- capturedOptions = opts.options;
277
- return {
278
- // eslint-disable-next-line @typescript-eslint/require-await
279
- async *[Symbol.asyncIterator]() {
280
- yield {
281
- type: 'result',
282
- subtype: 'success',
283
- result: '{"status":"success","message":"ok"}',
284
- };
285
- },
286
- };
287
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
288
- }),
289
- });
290
- await executeHook(makeBinding(), makeContext(), false, deps);
291
- assert.strictEqual(capturedOptions.model, 'haiku');
292
- assert.strictEqual(capturedOptions.maxTurns, 5);
293
- });
294
- void it('uses DEFAULT_MODEL when frontmatter has no model', async () => {
295
- let capturedOptions = {};
296
- const deps = makeDeps({
297
- loadSkillFile: () => Promise.resolve({
298
- frontmatter: {},
299
- body: 'Test prompt',
300
- }),
301
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
- queryFn: ((opts) => {
303
- capturedOptions = opts.options;
304
- return {
305
- // eslint-disable-next-line @typescript-eslint/require-await
306
- async *[Symbol.asyncIterator]() {
307
- yield {
308
- type: 'result',
309
- subtype: 'success',
310
- result: '{"status":"success","message":"ok"}',
311
- };
312
- },
313
- };
314
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
315
- }),
316
- });
317
- await executeHook(makeBinding(), makeContext(), false, deps);
318
- // DEFAULT_MODEL is 'opus'
319
- assert.strictEqual(capturedOptions.model, 'opus');
320
- });
321
- });
@@ -1,261 +0,0 @@
1
- import assert from 'node:assert';
2
- import { describe, it } from 'node:test';
3
- import { runHooksForPhase } from '../hook-runner.js';
4
- // ---- Helpers ----
5
- function makeBinding(overrides = {}) {
6
- return {
7
- id: 'hook-1',
8
- product_id: 'prod-1',
9
- phase: 'technical-design',
10
- hook_point: 'after',
11
- plugin_name: 'payload-cms',
12
- skill_name: 'validate-schema',
13
- on_failure: 'block',
14
- config: {},
15
- sort_order: 0,
16
- enabled: true,
17
- ...overrides,
18
- };
19
- }
20
- function makeResult(binding, overrides = {}) {
21
- return {
22
- hookId: binding.id,
23
- binding,
24
- status: 'success',
25
- message: 'OK',
26
- duration_ms: 100,
27
- ...overrides,
28
- };
29
- }
30
- function makeContext(overrides = {}) {
31
- return {
32
- featureId: 'feat-1',
33
- phase: 'technical-design',
34
- hookPoint: 'after',
35
- ...overrides,
36
- };
37
- }
38
- function makeDeps(overrides = {}) {
39
- return {
40
- executeHook: (binding) => Promise.resolve(makeResult(binding)),
41
- getCachedBindings: () => null,
42
- getBindingsForPhase: () => [],
43
- logHookEvent: () => Promise.resolve(),
44
- ...overrides,
45
- };
46
- }
47
- // ---- Tests ----
48
- void describe('runHooksForPhase', () => {
49
- void it('returns empty results when no cached bindings', async () => {
50
- const deps = makeDeps({ getCachedBindings: () => null });
51
- const result = await runHooksForPhase(makeContext(), deps);
52
- assert.deepStrictEqual(result, { results: [], blocked: false });
53
- });
54
- void it('returns empty results when cache has empty bindings', async () => {
55
- const deps = makeDeps({
56
- getCachedBindings: () => ({
57
- productId: 'prod-1',
58
- bindings: [],
59
- fetchedAt: Date.now(),
60
- }),
61
- });
62
- const result = await runHooksForPhase(makeContext(), deps);
63
- assert.deepStrictEqual(result, { results: [], blocked: false });
64
- });
65
- void it('returns empty results when no bindings match the phase', async () => {
66
- const cached = {
67
- productId: 'prod-1',
68
- bindings: [makeBinding({ phase: 'code-review' })],
69
- fetchedAt: Date.now(),
70
- };
71
- const deps = makeDeps({
72
- getCachedBindings: () => cached,
73
- getBindingsForPhase: () => [], // no match
74
- });
75
- const result = await runHooksForPhase(makeContext(), deps);
76
- assert.deepStrictEqual(result, { results: [], blocked: false });
77
- });
78
- void it('executes matching hooks and returns results', async () => {
79
- const binding = makeBinding();
80
- const cached = {
81
- productId: 'prod-1',
82
- bindings: [binding],
83
- fetchedAt: Date.now(),
84
- };
85
- const executeCalls = [];
86
- const deps = makeDeps({
87
- getCachedBindings: () => cached,
88
- getBindingsForPhase: () => [binding],
89
- executeHook: (b) => {
90
- executeCalls.push(b);
91
- return Promise.resolve(makeResult(b));
92
- },
93
- });
94
- const result = await runHooksForPhase(makeContext(), deps);
95
- assert.strictEqual(result.results.length, 1);
96
- assert.strictEqual(result.blocked, false);
97
- assert.strictEqual(executeCalls.length, 1);
98
- assert.strictEqual(executeCalls[0].id, 'hook-1');
99
- });
100
- void it('executes hooks in order (sequential)', async () => {
101
- const b1 = makeBinding({ id: 'first', sort_order: 0 });
102
- const b2 = makeBinding({ id: 'second', sort_order: 1 });
103
- const cached = {
104
- productId: 'prod-1',
105
- bindings: [b1, b2],
106
- fetchedAt: Date.now(),
107
- };
108
- const order = [];
109
- const deps = makeDeps({
110
- getCachedBindings: () => cached,
111
- getBindingsForPhase: () => [b1, b2],
112
- executeHook: (b) => {
113
- order.push(b.id);
114
- return Promise.resolve(makeResult(b));
115
- },
116
- });
117
- await runHooksForPhase(makeContext(), deps);
118
- assert.deepStrictEqual(order, ['first', 'second']);
119
- });
120
- void it('blocks on error with on_failure=block', async () => {
121
- const b1 = makeBinding({ id: 'blocker', on_failure: 'block' });
122
- const b2 = makeBinding({
123
- id: 'after-blocker',
124
- on_failure: 'block',
125
- sort_order: 1,
126
- });
127
- const cached = {
128
- productId: 'prod-1',
129
- bindings: [b1, b2],
130
- fetchedAt: Date.now(),
131
- };
132
- const executedIds = [];
133
- const deps = makeDeps({
134
- getCachedBindings: () => cached,
135
- getBindingsForPhase: () => [b1, b2],
136
- executeHook: (b) => {
137
- executedIds.push(b.id);
138
- if (b.id === 'blocker') {
139
- return Promise.resolve(makeResult(b, {
140
- status: 'error',
141
- message: 'Validation failed',
142
- }));
143
- }
144
- return Promise.resolve(makeResult(b));
145
- },
146
- });
147
- const result = await runHooksForPhase(makeContext(), deps);
148
- assert.strictEqual(result.blocked, true);
149
- assert.strictEqual(result.results.length, 1); // only the blocker ran
150
- assert.strictEqual(result.results[0].status, 'error');
151
- assert.deepStrictEqual(executedIds, ['blocker']); // second hook never ran
152
- });
153
- void it('continues on error with on_failure=warn', async () => {
154
- const b1 = makeBinding({ id: 'warner', on_failure: 'warn' });
155
- const b2 = makeBinding({
156
- id: 'after-warner',
157
- on_failure: 'block',
158
- sort_order: 1,
159
- });
160
- const cached = {
161
- productId: 'prod-1',
162
- bindings: [b1, b2],
163
- fetchedAt: Date.now(),
164
- };
165
- const deps = makeDeps({
166
- getCachedBindings: () => cached,
167
- getBindingsForPhase: () => [b1, b2],
168
- executeHook: (b) => {
169
- if (b.id === 'warner') {
170
- return Promise.resolve(makeResult(b, {
171
- status: 'error',
172
- message: 'Non-critical issue',
173
- }));
174
- }
175
- return Promise.resolve(makeResult(b));
176
- },
177
- });
178
- const result = await runHooksForPhase(makeContext(), deps);
179
- assert.strictEqual(result.blocked, false);
180
- assert.strictEqual(result.results.length, 2); // both ran
181
- assert.strictEqual(result.results[0].status, 'error');
182
- assert.strictEqual(result.results[1].status, 'success');
183
- });
184
- void it('continues silently on error with on_failure=skip', async () => {
185
- const b1 = makeBinding({ id: 'skipper', on_failure: 'skip' });
186
- const b2 = makeBinding({ id: 'after-skipper', sort_order: 1 });
187
- const cached = {
188
- productId: 'prod-1',
189
- bindings: [b1, b2],
190
- fetchedAt: Date.now(),
191
- };
192
- const deps = makeDeps({
193
- getCachedBindings: () => cached,
194
- getBindingsForPhase: () => [b1, b2],
195
- executeHook: (b) => {
196
- if (b.id === 'skipper') {
197
- return Promise.resolve(makeResult(b, { status: 'error', message: 'Ignored' }));
198
- }
199
- return Promise.resolve(makeResult(b));
200
- },
201
- });
202
- const result = await runHooksForPhase(makeContext(), deps);
203
- assert.strictEqual(result.blocked, false);
204
- assert.strictEqual(result.results.length, 2);
205
- });
206
- void it('logs events for each hook execution', async () => {
207
- const binding = makeBinding();
208
- const cached = {
209
- productId: 'prod-1',
210
- bindings: [binding],
211
- fetchedAt: Date.now(),
212
- };
213
- const logCalls = [];
214
- const deps = makeDeps({
215
- getCachedBindings: () => cached,
216
- getBindingsForPhase: () => [binding],
217
- logHookEvent: ({ result: r }) => {
218
- logCalls.push(r.hookId);
219
- return Promise.resolve();
220
- },
221
- });
222
- await runHooksForPhase(makeContext(), deps);
223
- assert.deepStrictEqual(logCalls, ['hook-1']);
224
- });
225
- void it('does not block when logHookEvent throws', async () => {
226
- const binding = makeBinding();
227
- const cached = {
228
- productId: 'prod-1',
229
- bindings: [binding],
230
- fetchedAt: Date.now(),
231
- };
232
- const deps = makeDeps({
233
- getCachedBindings: () => cached,
234
- getBindingsForPhase: () => [binding],
235
- logHookEvent: () => {
236
- return Promise.reject(new Error('Logging failed'));
237
- },
238
- });
239
- const result = await runHooksForPhase(makeContext(), deps);
240
- // Should still succeed — logging failure is swallowed
241
- assert.strictEqual(result.blocked, false);
242
- assert.strictEqual(result.results.length, 1);
243
- });
244
- void it('handles skipped hooks (file not found) without blocking', async () => {
245
- const binding = makeBinding({ on_failure: 'block' });
246
- const cached = {
247
- productId: 'prod-1',
248
- bindings: [binding],
249
- fetchedAt: Date.now(),
250
- };
251
- const deps = makeDeps({
252
- getCachedBindings: () => cached,
253
- getBindingsForPhase: () => [binding],
254
- executeHook: (b) => Promise.resolve(makeResult(b, { status: 'skipped', message: 'Not found' })),
255
- });
256
- const result = await runHooksForPhase(makeContext(), deps);
257
- // 'skipped' is not an error, so on_failure policy should not apply
258
- assert.strictEqual(result.blocked, false);
259
- assert.strictEqual(result.results[0].status, 'skipped');
260
- });
261
- });