edsger 0.45.0 → 0.46.0

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