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.
- package/dist/commands/workflow/executors/phase-executor.js +3 -1
- package/dist/commands/workflow/phase-orchestrator.js +1 -2
- package/dist/phases/app-store-generation/index.js +1 -2
- package/dist/phases/branch-planning/index.js +1 -2
- package/dist/phases/bug-fixing/analyzer.js +1 -2
- package/dist/phases/code-implementation/index.js +1 -2
- package/dist/phases/code-refine/index.js +1 -2
- package/dist/phases/code-review/index.js +1 -2
- package/dist/phases/code-testing/analyzer.js +1 -2
- package/dist/phases/feature-analysis/index.js +1 -2
- package/dist/phases/functional-testing/analyzer.js +1 -2
- package/dist/phases/growth-analysis/index.js +1 -2
- package/dist/phases/pr-execution/index.js +1 -0
- package/dist/phases/pr-splitting/index.js +1 -2
- package/dist/phases/run-sheet/index.js +7 -7
- package/dist/phases/run-sheet/render.js +3 -1
- package/dist/phases/smoke-test/agent.js +2 -4
- package/dist/phases/smoke-test/index.js +11 -6
- package/dist/phases/technical-design/index.js +1 -2
- package/dist/phases/test-cases-analysis/index.js +1 -2
- package/dist/phases/user-stories-analysis/index.js +1 -2
- package/package.json +3 -3
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +3 -9
- package/dist/api/__tests__/app-store.test.d.ts +0 -7
- package/dist/api/__tests__/app-store.test.js +0 -60
- package/dist/api/__tests__/intelligence.test.d.ts +0 -11
- package/dist/api/__tests__/intelligence.test.js +0 -315
- package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
- package/dist/api/features/__tests__/feature-utils.test.js +0 -370
- package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
- package/dist/api/features/__tests__/status-updater.test.js +0 -88
- package/dist/commands/build/__tests__/build.test.d.ts +0 -5
- package/dist/commands/build/__tests__/build.test.js +0 -206
- package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
- package/dist/commands/build/__tests__/detect-project.test.js +0 -160
- package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
- package/dist/commands/build/__tests__/run-build.test.js +0 -433
- package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
- package/dist/commands/intelligence/__tests__/command.test.js +0 -48
- package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
- package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
- package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
- package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
- package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
- package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
- package/dist/config/__tests__/config.test.d.ts +0 -4
- package/dist/config/__tests__/config.test.js +0 -286
- package/dist/config/__tests__/feature-status.test.d.ts +0 -4
- package/dist/config/__tests__/feature-status.test.js +0 -111
- package/dist/errors/__tests__/index.test.d.ts +0 -4
- package/dist/errors/__tests__/index.test.js +0 -349
- package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
- package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
- package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
- package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
- package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
- package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
- package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
- package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
- package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
- package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
- package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
- package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
- package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
- package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
- package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
- package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
- package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
- package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
- package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
- package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
- package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
- package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
- package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
- package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
- package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
- package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
- package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
- package/dist/phases/release-sync/__tests__/github.test.d.ts +0 -9
- package/dist/phases/release-sync/__tests__/github.test.js +0 -123
- package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +0 -8
- package/dist/phases/release-sync/__tests__/snapshot.test.js +0 -93
- package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
- package/dist/phases/smoke-test/__tests__/agent.test.js +0 -85
- package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
- package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
- package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
- package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
- package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
- package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
- package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
- package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
- package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
- package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
- package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
- package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
- package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
- 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 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|