@dtlabs/ai-toolkit 0.9.0 → 0.10.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/.claude/agents/generate-work-breakdown.md +212 -192
- package/.claude/agents/install-toolkit.md +6 -1
- package/.claude/agents/validate-work-breakdown-semantic.md +96 -0
- package/.claude/scripts/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/ac-table-format.md +48 -0
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-Work-Breakdown.json +234 -0
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-semantic-output.json +4 -0
- package/.claude/scripts/tests/fixtures/test-feature/feature.md +45 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-bad-domain.json +39 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-cycle.json +61 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-duplicate-id.json +72 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-missing-field.json +38 -0
- package/.claude/scripts/tests/fixtures/wb-valid.json +189 -0
- package/.claude/scripts/tests/helpers/buildWaves.js +21 -0
- package/.claude/scripts/tests/helpers/pm-phase2-flow.js +29 -0
- package/.claude/scripts/tests/helpers/pm-phase2-gate2.js +55 -0
- package/.claude/scripts/tests/helpers/pm-phase2-ledger.js +108 -0
- package/.claude/scripts/tests/installer.scripts-distribution.test.js +131 -0
- package/.claude/scripts/tests/pm-phase2.gate2-payload.test.js +353 -0
- package/.claude/scripts/tests/pm-phase2.ledger-transitions.test.js +346 -0
- package/.claude/scripts/tests/pm-phase2.orchestration-flow.test.js +276 -0
- package/.claude/scripts/tests/verify-us-01-t03.js +288 -0
- package/.claude/scripts/tests/verify-us-03-t04.js +262 -0
- package/.claude/scripts/tests/wb-render.checks-md-csv-deps.test.js +337 -0
- package/.claude/scripts/tests/wb-render.checks-sanitize-commit-paths.test.js +352 -0
- package/.claude/scripts/tests/wb-render.regression-pm-phase3-csv.test.js +219 -0
- package/.claude/scripts/tests/wb-validate.checks-1-7.test.js +330 -0
- package/.claude/scripts/tests/wb-validate.checks-11.test.js +166 -0
- package/.claude/scripts/tests/wb-validate.checks-12-14.test.js +222 -0
- package/.claude/scripts/tests/wb-validate.checks-15-18.test.js +359 -0
- package/.claude/scripts/tests/wb-validate.checks-19-21.test.js +362 -0
- package/.claude/scripts/tests/wb-validate.checks-22-23-report.test.js +297 -0
- package/.claude/scripts/tests/wb-validate.checks-8-10.test.js +243 -0
- package/.claude/scripts/wb-render.js +309 -0
- package/.claude/scripts/wb-validate.js +1007 -0
- package/.claude/workflows/pm-phase2.js +289 -28
- package/CHANGELOG.md +67 -0
- package/bin/cli.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { buildGate2Payload } = require('./helpers/pm-phase2-gate2');
|
|
4
|
+
|
|
5
|
+
// ─── shared fixtures ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
// Minimal report that satisfies wbValidatorPassed (valid=true, empty errors array)
|
|
8
|
+
const REPORT_CLEAN = { valid: true, errors: [] };
|
|
9
|
+
|
|
10
|
+
// A report that fails structural validation (valid=false)
|
|
11
|
+
const REPORT_INVALID = { valid: false, errors: [{ category: 'schema_version_invalid', message: 'bad schema' }] };
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('pm-phase2 gate2_payload assembly — buildGate2Payload()', () => {
|
|
16
|
+
|
|
17
|
+
// ── Group 1: gate2_blocked flag ───────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
describe('gate2_blocked flag', () => {
|
|
20
|
+
|
|
21
|
+
test('is false when wb-validate passed, semantic ok, render ok, and no blocking findings', () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
const options = {
|
|
24
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
25
|
+
validateFailed: false,
|
|
26
|
+
semanticResult: { findings: [{ blocking: false, message: 'style suggestion' }] },
|
|
27
|
+
semanticFailed: false,
|
|
28
|
+
renderResult: { ok: true },
|
|
29
|
+
renderFailed: false,
|
|
30
|
+
};
|
|
31
|
+
// Act
|
|
32
|
+
const payload = buildGate2Payload(options);
|
|
33
|
+
// Assert
|
|
34
|
+
expect(payload.gate2_blocked).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('is true when validateFailed is true (wb-validate technical failure)', () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const options = {
|
|
40
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
41
|
+
validateFailed: true,
|
|
42
|
+
semanticFailed: false,
|
|
43
|
+
renderFailed: false,
|
|
44
|
+
semanticResult: null,
|
|
45
|
+
};
|
|
46
|
+
// Act
|
|
47
|
+
const payload = buildGate2Payload(options);
|
|
48
|
+
// Assert
|
|
49
|
+
expect(payload.gate2_blocked).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('is true when wb-validate exits 1 with valid=false in report', () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const options = {
|
|
55
|
+
wbValidatorReport: REPORT_INVALID,
|
|
56
|
+
validateFailed: false,
|
|
57
|
+
semanticFailed: false,
|
|
58
|
+
renderFailed: false,
|
|
59
|
+
semanticResult: null,
|
|
60
|
+
};
|
|
61
|
+
// Act
|
|
62
|
+
const payload = buildGate2Payload(options);
|
|
63
|
+
// Assert
|
|
64
|
+
expect(payload.gate2_blocked).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('is true when semanticFailed is true', () => {
|
|
68
|
+
// Arrange
|
|
69
|
+
const options = {
|
|
70
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
71
|
+
validateFailed: false,
|
|
72
|
+
semanticFailed: true,
|
|
73
|
+
renderFailed: false,
|
|
74
|
+
semanticResult: null,
|
|
75
|
+
};
|
|
76
|
+
// Act
|
|
77
|
+
const payload = buildGate2Payload(options);
|
|
78
|
+
// Assert
|
|
79
|
+
expect(payload.gate2_blocked).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('is true when renderFailed is true', () => {
|
|
83
|
+
// Arrange
|
|
84
|
+
const options = {
|
|
85
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
86
|
+
validateFailed: false,
|
|
87
|
+
semanticFailed: false,
|
|
88
|
+
renderFailed: true,
|
|
89
|
+
semanticResult: null,
|
|
90
|
+
};
|
|
91
|
+
// Act
|
|
92
|
+
const payload = buildGate2Payload(options);
|
|
93
|
+
// Assert
|
|
94
|
+
expect(payload.gate2_blocked).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('is true when semantic result contains at least one blocking finding', () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const options = {
|
|
100
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
101
|
+
validateFailed: false,
|
|
102
|
+
semanticFailed: false,
|
|
103
|
+
renderFailed: false,
|
|
104
|
+
semanticResult: { findings: [{ blocking: true, message: 'critical scope gap' }] },
|
|
105
|
+
};
|
|
106
|
+
// Act
|
|
107
|
+
const payload = buildGate2Payload(options);
|
|
108
|
+
// Assert
|
|
109
|
+
expect(payload.gate2_blocked).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('is false when semantic result has only non-blocking (warning-level) findings', () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const options = {
|
|
115
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
116
|
+
validateFailed: false,
|
|
117
|
+
semanticFailed: false,
|
|
118
|
+
renderFailed: false,
|
|
119
|
+
semanticResult: {
|
|
120
|
+
findings: [
|
|
121
|
+
{ blocking: false, message: 'minor wording suggestion' },
|
|
122
|
+
{ blocking: false, message: 'optional improvement' },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
// Act
|
|
127
|
+
const payload = buildGate2Payload(options);
|
|
128
|
+
// Assert
|
|
129
|
+
expect(payload.gate2_blocked).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Group 2: warning_band_tasks derivation ────────────────────────────────
|
|
135
|
+
|
|
136
|
+
describe('warning_band_tasks derivation', () => {
|
|
137
|
+
|
|
138
|
+
test('has one entry with correct taskId and agentMinutes when report has one duration_warning', () => {
|
|
139
|
+
// Arrange — fixture uses agentMinutes: the canonical field name emitted by wb-validate.js check 15
|
|
140
|
+
const options = {
|
|
141
|
+
wbValidatorReport: {
|
|
142
|
+
valid: true,
|
|
143
|
+
errors: [],
|
|
144
|
+
warnings: [
|
|
145
|
+
{ category: 'duration_warning', taskId: 'US-01-T03', details: { agentMinutes: 110, targetMax: 15, warningMax: 30 } },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
validateFailed: false,
|
|
149
|
+
};
|
|
150
|
+
// Act
|
|
151
|
+
const payload = buildGate2Payload(options);
|
|
152
|
+
// Assert
|
|
153
|
+
expect(payload.warning_band_tasks).toHaveLength(1);
|
|
154
|
+
expect(payload.warning_band_tasks[0]).toEqual({ taskId: 'US-01-T03', agentMinutes: 110 });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('is an empty array when report has no warnings', () => {
|
|
158
|
+
// Arrange
|
|
159
|
+
const options = {
|
|
160
|
+
wbValidatorReport: { valid: true, errors: [], warnings: [] },
|
|
161
|
+
validateFailed: false,
|
|
162
|
+
};
|
|
163
|
+
// Act
|
|
164
|
+
const payload = buildGate2Payload(options);
|
|
165
|
+
// Assert
|
|
166
|
+
expect(payload.warning_band_tasks).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('is an empty array when report is null', () => {
|
|
170
|
+
// Arrange
|
|
171
|
+
const options = {
|
|
172
|
+
wbValidatorReport: null,
|
|
173
|
+
validateFailed: false,
|
|
174
|
+
};
|
|
175
|
+
// Act
|
|
176
|
+
const payload = buildGate2Payload(options);
|
|
177
|
+
// Assert
|
|
178
|
+
expect(payload.warning_band_tasks).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── Group 3: split_required_tasks derivation ──────────────────────────────
|
|
184
|
+
|
|
185
|
+
describe('split_required_tasks derivation', () => {
|
|
186
|
+
|
|
187
|
+
test('has one entry with taskId and agentMinutes when report has one split_required error', () => {
|
|
188
|
+
// Arrange — fixture uses agentMinutes: the canonical field name emitted by wb-validate.js check 15
|
|
189
|
+
const options = {
|
|
190
|
+
wbValidatorReport: {
|
|
191
|
+
valid: false,
|
|
192
|
+
errors: [
|
|
193
|
+
{ category: 'split_required', taskId: 'US-02-T01', details: { agentMinutes: 200, warningMax: 30 } },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
validateFailed: false,
|
|
197
|
+
};
|
|
198
|
+
// Act
|
|
199
|
+
const payload = buildGate2Payload(options);
|
|
200
|
+
// Assert
|
|
201
|
+
expect(payload.split_required_tasks).toHaveLength(1);
|
|
202
|
+
expect(payload.split_required_tasks[0]).toEqual({ taskId: 'US-02-T01', agentMinutes: 200 });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('is an empty array when report errors have no split_required category entries', () => {
|
|
206
|
+
// Arrange
|
|
207
|
+
const options = {
|
|
208
|
+
wbValidatorReport: {
|
|
209
|
+
valid: false,
|
|
210
|
+
errors: [
|
|
211
|
+
{ category: 'must_ac_uncovered', taskId: 'US-01-T01', details: { acId: 'AC-01' } },
|
|
212
|
+
{ category: 'schema_version_invalid', taskId: null, details: null },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
validateFailed: false,
|
|
216
|
+
};
|
|
217
|
+
// Act
|
|
218
|
+
const payload = buildGate2Payload(options);
|
|
219
|
+
// Assert
|
|
220
|
+
expect(payload.split_required_tasks).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('is an empty array when report is null', () => {
|
|
224
|
+
// Arrange
|
|
225
|
+
const options = {
|
|
226
|
+
wbValidatorReport: null,
|
|
227
|
+
validateFailed: false,
|
|
228
|
+
};
|
|
229
|
+
// Act
|
|
230
|
+
const payload = buildGate2Payload(options);
|
|
231
|
+
// Assert
|
|
232
|
+
expect(payload.split_required_tasks).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── Group 4: must_ac_uncovered derivation ─────────────────────────────────
|
|
238
|
+
|
|
239
|
+
describe('must_ac_uncovered derivation', () => {
|
|
240
|
+
|
|
241
|
+
test('has one entry with acId when report has one must_ac_uncovered error', () => {
|
|
242
|
+
// Arrange
|
|
243
|
+
const options = {
|
|
244
|
+
wbValidatorReport: {
|
|
245
|
+
valid: false,
|
|
246
|
+
errors: [
|
|
247
|
+
{ category: 'must_ac_uncovered', taskId: null, details: { acId: 'AC-03' } },
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
validateFailed: false,
|
|
251
|
+
};
|
|
252
|
+
// Act
|
|
253
|
+
const payload = buildGate2Payload(options);
|
|
254
|
+
// Assert
|
|
255
|
+
expect(payload.must_ac_uncovered).toHaveLength(1);
|
|
256
|
+
expect(payload.must_ac_uncovered[0]).toEqual({ acId: 'AC-03' });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('is an empty array when report is null', () => {
|
|
260
|
+
// Arrange
|
|
261
|
+
const options = {
|
|
262
|
+
wbValidatorReport: null,
|
|
263
|
+
validateFailed: false,
|
|
264
|
+
};
|
|
265
|
+
// Act
|
|
266
|
+
const payload = buildGate2Payload(options);
|
|
267
|
+
// Assert
|
|
268
|
+
expect(payload.must_ac_uncovered).toEqual([]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ── Group 5: phase_unschedulable derivation ───────────────────────────────
|
|
274
|
+
|
|
275
|
+
describe('phase_unschedulable derivation', () => {
|
|
276
|
+
|
|
277
|
+
test('contains the unschedulable phase id when report dependencies flag one phase', () => {
|
|
278
|
+
// Arrange
|
|
279
|
+
const options = {
|
|
280
|
+
wbValidatorReport: {
|
|
281
|
+
valid: true,
|
|
282
|
+
errors: [],
|
|
283
|
+
dependencies: { phaseUnschedulable: ['Phase-2'] },
|
|
284
|
+
},
|
|
285
|
+
validateFailed: false,
|
|
286
|
+
};
|
|
287
|
+
// Act
|
|
288
|
+
const payload = buildGate2Payload(options);
|
|
289
|
+
// Assert
|
|
290
|
+
expect(payload.phase_unschedulable).toContain('Phase-2');
|
|
291
|
+
expect(payload.phase_unschedulable).toHaveLength(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('is an empty array when report is null', () => {
|
|
295
|
+
// Arrange
|
|
296
|
+
const options = {
|
|
297
|
+
wbValidatorReport: null,
|
|
298
|
+
validateFailed: false,
|
|
299
|
+
};
|
|
300
|
+
// Act
|
|
301
|
+
const payload = buildGate2Payload(options);
|
|
302
|
+
// Assert
|
|
303
|
+
expect(payload.phase_unschedulable).toEqual([]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── Group 6: duration_bands and domain_distribution passthrough ───────────
|
|
309
|
+
|
|
310
|
+
describe('duration_bands and domain_distribution passthrough', () => {
|
|
311
|
+
|
|
312
|
+
test('duration_bands is the durationBands object from the report', () => {
|
|
313
|
+
// Arrange
|
|
314
|
+
const durationBands = { target: 5, aboveTarget: 2, warning: 1, splitRequired: 0 };
|
|
315
|
+
const options = {
|
|
316
|
+
wbValidatorReport: { valid: true, errors: [], durationBands },
|
|
317
|
+
validateFailed: false,
|
|
318
|
+
};
|
|
319
|
+
// Act
|
|
320
|
+
const payload = buildGate2Payload(options);
|
|
321
|
+
// Assert
|
|
322
|
+
expect(payload.duration_bands).toEqual(durationBands);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('domain_distribution is the domainDistribution object from the report', () => {
|
|
326
|
+
// Arrange
|
|
327
|
+
const domainDistribution = { BE: 3 };
|
|
328
|
+
const options = {
|
|
329
|
+
wbValidatorReport: { valid: true, errors: [], domainDistribution },
|
|
330
|
+
validateFailed: false,
|
|
331
|
+
};
|
|
332
|
+
// Act
|
|
333
|
+
const payload = buildGate2Payload(options);
|
|
334
|
+
// Assert
|
|
335
|
+
expect(payload.domain_distribution).toEqual(domainDistribution);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('duration_bands and domain_distribution are both null when report is null', () => {
|
|
339
|
+
// Arrange
|
|
340
|
+
const options = {
|
|
341
|
+
wbValidatorReport: null,
|
|
342
|
+
validateFailed: false,
|
|
343
|
+
};
|
|
344
|
+
// Act
|
|
345
|
+
const payload = buildGate2Payload(options);
|
|
346
|
+
// Assert
|
|
347
|
+
expect(payload.duration_bands).toBeNull();
|
|
348
|
+
expect(payload.domain_distribution).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
});
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { simulateLedgerEntries } = require('./helpers/pm-phase2-ledger');
|
|
4
|
+
|
|
5
|
+
// ─── shared fixtures ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
// Minimal report that satisfies wbValidatorPassed (valid=true, empty errors array)
|
|
8
|
+
const REPORT_CLEAN = { valid: true, errors: [] };
|
|
9
|
+
|
|
10
|
+
// A report that reflects exit code 1 — validator ran but found structural errors
|
|
11
|
+
const REPORT_INVALID = { valid: false, errors: [{ category: 'missing_field', message: 'outcome missing' }] };
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('pm-phase2 ledger state machine — simulateLedgerEntries()', () => {
|
|
16
|
+
|
|
17
|
+
// ── Group 1: wb-validate ledger entries ───────────────────────────────────
|
|
18
|
+
|
|
19
|
+
describe('wb-validate ledger entries', () => {
|
|
20
|
+
|
|
21
|
+
test('wb-validate passes → entry has final_status done and carries actual token count', () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
const scenario = {
|
|
24
|
+
validateFailed: false,
|
|
25
|
+
validateTokens: 500,
|
|
26
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
27
|
+
};
|
|
28
|
+
// Act
|
|
29
|
+
const entries = simulateLedgerEntries(scenario);
|
|
30
|
+
// Assert
|
|
31
|
+
expect(entries[0].final_status).toBe('done');
|
|
32
|
+
expect(entries[0].phase_delta_tokens).toBe(500);
|
|
33
|
+
expect(entries[0].error_summary).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('wb-validate fails technically → entry has final_status failed with error_summary and exit_code', () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const scenario = {
|
|
39
|
+
validateFailed: true,
|
|
40
|
+
validateError: 'Script crashed',
|
|
41
|
+
validateExitCode: 2,
|
|
42
|
+
validateTokens: 100,
|
|
43
|
+
wbValidatorReport: null,
|
|
44
|
+
};
|
|
45
|
+
// Act
|
|
46
|
+
const entries = simulateLedgerEntries(scenario);
|
|
47
|
+
// Assert
|
|
48
|
+
expect(entries[0].final_status).toBe('failed');
|
|
49
|
+
expect(entries[0].error_summary).toBe('Script crashed');
|
|
50
|
+
expect(entries[0].exit_code).toBe(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('wb-validate exit 1 (structural errors) → entry has final_status done (completed run, not a failure)', () => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const scenario = {
|
|
56
|
+
validateFailed: false,
|
|
57
|
+
validateTokens: 300,
|
|
58
|
+
wbValidatorReport: REPORT_INVALID,
|
|
59
|
+
};
|
|
60
|
+
// Act
|
|
61
|
+
const entries = simulateLedgerEntries(scenario);
|
|
62
|
+
// Assert
|
|
63
|
+
expect(entries[0].final_status).toBe('done');
|
|
64
|
+
expect(entries[0].error_summary).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('wb-validate status_sequence always starts with running — never skipped', () => {
|
|
68
|
+
// Arrange — both success and failure scenarios
|
|
69
|
+
const successScenario = { validateFailed: false, wbValidatorReport: REPORT_CLEAN };
|
|
70
|
+
const failureScenario = { validateFailed: true, wbValidatorReport: null };
|
|
71
|
+
// Act
|
|
72
|
+
const successEntries = simulateLedgerEntries(successScenario);
|
|
73
|
+
const failureEntries = simulateLedgerEntries(failureScenario);
|
|
74
|
+
// Assert
|
|
75
|
+
expect(successEntries[0].status_sequence[0]).toBe('running');
|
|
76
|
+
expect(failureEntries[0].status_sequence[0]).toBe('running');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── Group 2: semantic validator ledger entries ────────────────────────────
|
|
82
|
+
|
|
83
|
+
describe('semantic validator ledger entries', () => {
|
|
84
|
+
|
|
85
|
+
test('wb-validate failed → semantic entry has final_status skipped with zero tokens and no error_summary', () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const scenario = {
|
|
88
|
+
validateFailed: true,
|
|
89
|
+
validateExitCode: 1,
|
|
90
|
+
wbValidatorReport: null,
|
|
91
|
+
};
|
|
92
|
+
// Act
|
|
93
|
+
const entries = simulateLedgerEntries(scenario);
|
|
94
|
+
// Assert
|
|
95
|
+
expect(entries[1].final_status).toBe('skipped');
|
|
96
|
+
expect(entries[1].phase_delta_tokens).toBe(0);
|
|
97
|
+
expect(entries[1].error_summary).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('wb-validate exit 1 with structural errors → semantic entry has final_status skipped (wbValidatorPassed is false)', () => {
|
|
101
|
+
// Arrange
|
|
102
|
+
const scenario = {
|
|
103
|
+
validateFailed: false,
|
|
104
|
+
wbValidatorReport: REPORT_INVALID,
|
|
105
|
+
};
|
|
106
|
+
// Act
|
|
107
|
+
const entries = simulateLedgerEntries(scenario);
|
|
108
|
+
// Assert
|
|
109
|
+
expect(entries[1].final_status).toBe('skipped');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('wb-validate passed and semantic succeeds → semantic entry has final_status done with actual token count', () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const scenario = {
|
|
115
|
+
validateFailed: false,
|
|
116
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
117
|
+
semanticTokens: 1200,
|
|
118
|
+
};
|
|
119
|
+
// Act
|
|
120
|
+
const entries = simulateLedgerEntries(scenario);
|
|
121
|
+
// Assert
|
|
122
|
+
expect(entries[1].final_status).toBe('done');
|
|
123
|
+
expect(entries[1].phase_delta_tokens).toBe(1200);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('wb-validate passed and semantic fails → semantic entry has final_status failed with error_summary and exit_code null', () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
const scenario = {
|
|
129
|
+
validateFailed: false,
|
|
130
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
131
|
+
semanticFailed: true,
|
|
132
|
+
semanticError: 'Agent timeout',
|
|
133
|
+
semanticTokens: 800,
|
|
134
|
+
};
|
|
135
|
+
// Act
|
|
136
|
+
const entries = simulateLedgerEntries(scenario);
|
|
137
|
+
// Assert
|
|
138
|
+
expect(entries[1].final_status).toBe('failed');
|
|
139
|
+
expect(entries[1].error_summary).toBe('Agent timeout');
|
|
140
|
+
expect(entries[1].exit_code).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('skipped semantic entry always has phase_delta_tokens of zero regardless of semanticTokens in scenario', () => {
|
|
144
|
+
// Arrange — wb-validate failed forces semantic to be skipped
|
|
145
|
+
const scenario = {
|
|
146
|
+
validateFailed: true,
|
|
147
|
+
wbValidatorReport: null,
|
|
148
|
+
semanticTokens: 999,
|
|
149
|
+
};
|
|
150
|
+
// Act
|
|
151
|
+
const entries = simulateLedgerEntries(scenario);
|
|
152
|
+
// Assert
|
|
153
|
+
expect(entries[1].phase_delta_tokens).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── Group 3: wb-render ledger entries ────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe('wb-render ledger entries', () => {
|
|
161
|
+
|
|
162
|
+
test('wb-validate failed → render entry has final_status skipped', () => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const scenario = {
|
|
165
|
+
validateFailed: true,
|
|
166
|
+
wbValidatorReport: null,
|
|
167
|
+
};
|
|
168
|
+
// Act
|
|
169
|
+
const entries = simulateLedgerEntries(scenario);
|
|
170
|
+
// Assert
|
|
171
|
+
expect(entries[2].final_status).toBe('skipped');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('wb-validate exit 1 (structural errors) → render entry has final_status skipped', () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
const scenario = {
|
|
177
|
+
validateFailed: false,
|
|
178
|
+
wbValidatorReport: REPORT_INVALID,
|
|
179
|
+
};
|
|
180
|
+
// Act
|
|
181
|
+
const entries = simulateLedgerEntries(scenario);
|
|
182
|
+
// Assert
|
|
183
|
+
expect(entries[2].final_status).toBe('skipped');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('wb-validate passed but semantic failed → render entry has final_status skipped', () => {
|
|
187
|
+
// Arrange
|
|
188
|
+
const scenario = {
|
|
189
|
+
validateFailed: false,
|
|
190
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
191
|
+
semanticFailed: true,
|
|
192
|
+
semanticError: 'Critical gap found',
|
|
193
|
+
};
|
|
194
|
+
// Act
|
|
195
|
+
const entries = simulateLedgerEntries(scenario);
|
|
196
|
+
// Assert
|
|
197
|
+
expect(entries[2].final_status).toBe('skipped');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('all steps passed → render entry has final_status done with actual token count', () => {
|
|
201
|
+
// Arrange
|
|
202
|
+
const scenario = {
|
|
203
|
+
validateFailed: false,
|
|
204
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
205
|
+
renderTokens: 600,
|
|
206
|
+
};
|
|
207
|
+
// Act
|
|
208
|
+
const entries = simulateLedgerEntries(scenario);
|
|
209
|
+
// Assert
|
|
210
|
+
expect(entries[2].final_status).toBe('done');
|
|
211
|
+
expect(entries[2].phase_delta_tokens).toBe(600);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('render fails → entry has final_status failed with error_summary and exit_code from renderExitCode', () => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const scenario = {
|
|
217
|
+
validateFailed: false,
|
|
218
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
219
|
+
renderFailed: true,
|
|
220
|
+
renderError: 'Template not found',
|
|
221
|
+
renderExitCode: 3,
|
|
222
|
+
renderTokens: 400,
|
|
223
|
+
};
|
|
224
|
+
// Act
|
|
225
|
+
const entries = simulateLedgerEntries(scenario);
|
|
226
|
+
// Assert
|
|
227
|
+
expect(entries[2].final_status).toBe('failed');
|
|
228
|
+
expect(entries[2].error_summary).toBe('Template not found');
|
|
229
|
+
expect(entries[2].exit_code).toBe(3);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── Group 4: token attribution ────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe('token attribution', () => {
|
|
237
|
+
|
|
238
|
+
test('done entry carries the actual tokens consumed by the agent', () => {
|
|
239
|
+
// Arrange
|
|
240
|
+
const scenario = {
|
|
241
|
+
validateFailed: false,
|
|
242
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
243
|
+
validateTokens: 750,
|
|
244
|
+
};
|
|
245
|
+
// Act
|
|
246
|
+
const entries = simulateLedgerEntries(scenario);
|
|
247
|
+
// Assert
|
|
248
|
+
expect(entries[0].phase_delta_tokens).toBe(750);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('failed entry carries the actual tokens consumed before the failure — not zero', () => {
|
|
252
|
+
// Arrange
|
|
253
|
+
const scenario = {
|
|
254
|
+
validateFailed: true,
|
|
255
|
+
validateTokens: 250,
|
|
256
|
+
validateError: 'crash',
|
|
257
|
+
validateExitCode: 1,
|
|
258
|
+
wbValidatorReport: null,
|
|
259
|
+
};
|
|
260
|
+
// Act
|
|
261
|
+
const entries = simulateLedgerEntries(scenario);
|
|
262
|
+
// Assert
|
|
263
|
+
expect(entries[0].phase_delta_tokens).toBe(250);
|
|
264
|
+
expect(entries[0].phase_delta_tokens).toBeGreaterThan(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('all skipped entries have phase_delta_tokens of zero regardless of scenario token values', () => {
|
|
268
|
+
// Arrange — wb-validate failure causes both semantic and render to be skipped
|
|
269
|
+
const scenario = {
|
|
270
|
+
validateFailed: true,
|
|
271
|
+
wbValidatorReport: null,
|
|
272
|
+
validateTokens: 999,
|
|
273
|
+
semanticTokens: 999,
|
|
274
|
+
renderTokens: 999,
|
|
275
|
+
};
|
|
276
|
+
// Act
|
|
277
|
+
const entries = simulateLedgerEntries(scenario);
|
|
278
|
+
// Assert — entries[1] and entries[2] are both skipped
|
|
279
|
+
expect(entries[1].phase_delta_tokens).toBe(0);
|
|
280
|
+
expect(entries[2].phase_delta_tokens).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ── Group 5: exit_code attribution ───────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
describe('exit_code attribution', () => {
|
|
288
|
+
|
|
289
|
+
test('wb-validate technical failure with exit_code 2 → entry has exit_code 2', () => {
|
|
290
|
+
// Arrange
|
|
291
|
+
const scenario = {
|
|
292
|
+
validateFailed: true,
|
|
293
|
+
validateExitCode: 2,
|
|
294
|
+
wbValidatorReport: null,
|
|
295
|
+
};
|
|
296
|
+
// Act
|
|
297
|
+
const entries = simulateLedgerEntries(scenario);
|
|
298
|
+
// Assert
|
|
299
|
+
expect(entries[0].exit_code).toBe(2);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('wb-validate technical failure with null exit_code → entry has exit_code null', () => {
|
|
303
|
+
// Arrange
|
|
304
|
+
const scenario = {
|
|
305
|
+
validateFailed: true,
|
|
306
|
+
validateExitCode: null,
|
|
307
|
+
wbValidatorReport: null,
|
|
308
|
+
};
|
|
309
|
+
// Act
|
|
310
|
+
const entries = simulateLedgerEntries(scenario);
|
|
311
|
+
// Assert
|
|
312
|
+
expect(entries[0].exit_code).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('render failure → entry has exit_code taken from renderExitCode', () => {
|
|
316
|
+
// Arrange
|
|
317
|
+
const scenario = {
|
|
318
|
+
validateFailed: false,
|
|
319
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
320
|
+
renderFailed: true,
|
|
321
|
+
renderError: 'crash',
|
|
322
|
+
renderExitCode: 5,
|
|
323
|
+
};
|
|
324
|
+
// Act
|
|
325
|
+
const entries = simulateLedgerEntries(scenario);
|
|
326
|
+
// Assert
|
|
327
|
+
expect(entries[2].exit_code).toBe(5);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('semantic failure → entry has exit_code null (agent invocation carries no script exit code)', () => {
|
|
331
|
+
// Arrange
|
|
332
|
+
const scenario = {
|
|
333
|
+
validateFailed: false,
|
|
334
|
+
wbValidatorReport: REPORT_CLEAN,
|
|
335
|
+
semanticFailed: true,
|
|
336
|
+
semanticError: 'Timeout',
|
|
337
|
+
};
|
|
338
|
+
// Act
|
|
339
|
+
const entries = simulateLedgerEntries(scenario);
|
|
340
|
+
// Assert
|
|
341
|
+
expect(entries[1].exit_code).toBeNull();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
});
|