edsger 0.45.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 (95) hide show
  1. package/package.json +3 -3
  2. package/tsconfig.build.json +4 -0
  3. package/tsconfig.json +3 -9
  4. package/dist/api/__tests__/app-store.test.d.ts +0 -7
  5. package/dist/api/__tests__/app-store.test.js +0 -60
  6. package/dist/api/__tests__/intelligence.test.d.ts +0 -11
  7. package/dist/api/__tests__/intelligence.test.js +0 -315
  8. package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
  9. package/dist/api/features/__tests__/feature-utils.test.js +0 -370
  10. package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
  11. package/dist/api/features/__tests__/status-updater.test.js +0 -88
  12. package/dist/commands/build/__tests__/build.test.d.ts +0 -5
  13. package/dist/commands/build/__tests__/build.test.js +0 -206
  14. package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
  15. package/dist/commands/build/__tests__/detect-project.test.js +0 -160
  16. package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
  17. package/dist/commands/build/__tests__/run-build.test.js +0 -433
  18. package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
  19. package/dist/commands/intelligence/__tests__/command.test.js +0 -48
  20. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
  21. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
  22. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
  23. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
  24. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
  25. package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
  26. package/dist/config/__tests__/config.test.d.ts +0 -4
  27. package/dist/config/__tests__/config.test.js +0 -286
  28. package/dist/config/__tests__/feature-status.test.d.ts +0 -4
  29. package/dist/config/__tests__/feature-status.test.js +0 -111
  30. package/dist/errors/__tests__/index.test.d.ts +0 -4
  31. package/dist/errors/__tests__/index.test.js +0 -349
  32. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
  33. package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
  34. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
  35. package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
  36. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
  37. package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
  38. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
  39. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
  40. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
  41. package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
  42. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
  43. package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
  44. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
  45. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
  46. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
  47. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
  48. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
  49. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
  50. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
  51. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
  52. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
  53. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
  54. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
  55. package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
  56. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
  57. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
  58. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
  59. package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
  60. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
  61. package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
  62. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
  63. package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
  64. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
  65. package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
  66. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
  67. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
  68. package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
  69. package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
  70. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
  71. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
  72. package/dist/phases/release-sync/__tests__/github.test.d.ts +0 -9
  73. package/dist/phases/release-sync/__tests__/github.test.js +0 -123
  74. package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +0 -8
  75. package/dist/phases/release-sync/__tests__/snapshot.test.js +0 -93
  76. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
  77. package/dist/phases/smoke-test/__tests__/agent.test.js +0 -85
  78. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
  79. package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
  80. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
  81. package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
  82. package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
  83. package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
  84. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
  85. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
  86. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
  87. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
  88. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
  89. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
  90. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
  91. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
  92. package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
  93. package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
  94. package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
  95. 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
- });