@dtlabs/ai-toolkit 0.10.0 → 0.10.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.
- package/CHANGELOG.md +8 -0
- package/bin/cli.js +16 -0
- package/package.json +7 -2
- package/.claude/.ai-toolkit-version +0 -1
- package/.claude/scripts/tests/fixtures/ac-table-format.md +0 -48
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-Work-Breakdown.json +0 -234
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-semantic-output.json +0 -4
- package/.claude/scripts/tests/fixtures/test-feature/feature.md +0 -45
- package/.claude/scripts/tests/fixtures/wb-invalid-bad-domain.json +0 -39
- package/.claude/scripts/tests/fixtures/wb-invalid-cycle.json +0 -61
- package/.claude/scripts/tests/fixtures/wb-invalid-duplicate-id.json +0 -72
- package/.claude/scripts/tests/fixtures/wb-invalid-missing-field.json +0 -38
- package/.claude/scripts/tests/fixtures/wb-valid.json +0 -189
- package/.claude/scripts/tests/helpers/buildWaves.js +0 -21
- package/.claude/scripts/tests/helpers/pm-phase2-flow.js +0 -29
- package/.claude/scripts/tests/helpers/pm-phase2-gate2.js +0 -55
- package/.claude/scripts/tests/helpers/pm-phase2-ledger.js +0 -108
- package/.claude/scripts/tests/installer.scripts-distribution.test.js +0 -131
- package/.claude/scripts/tests/pm-phase2.gate2-payload.test.js +0 -353
- package/.claude/scripts/tests/pm-phase2.ledger-transitions.test.js +0 -346
- package/.claude/scripts/tests/pm-phase2.orchestration-flow.test.js +0 -276
- package/.claude/scripts/tests/verify-us-01-t03.js +0 -288
- package/.claude/scripts/tests/verify-us-03-t04.js +0 -262
- package/.claude/scripts/tests/wb-render.checks-md-csv-deps.test.js +0 -337
- package/.claude/scripts/tests/wb-render.checks-sanitize-commit-paths.test.js +0 -352
- package/.claude/scripts/tests/wb-render.regression-pm-phase3-csv.test.js +0 -219
- package/.claude/scripts/tests/wb-validate.checks-1-7.test.js +0 -330
- package/.claude/scripts/tests/wb-validate.checks-11.test.js +0 -166
- package/.claude/scripts/tests/wb-validate.checks-12-14.test.js +0 -222
- package/.claude/scripts/tests/wb-validate.checks-15-18.test.js +0 -359
- package/.claude/scripts/tests/wb-validate.checks-19-21.test.js +0 -362
- package/.claude/scripts/tests/wb-validate.checks-22-23-report.test.js +0 -297
- package/.claude/scripts/tests/wb-validate.checks-8-10.test.js +0 -243
- package/.claude/settings.json +0 -5
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
|
|
8
|
-
const VALIDATE = path.resolve(__dirname, '../wb-validate.js');
|
|
9
|
-
const FIXTURES = path.resolve(__dirname, 'fixtures');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Run wb-validate.js as a subprocess and return exit code, stdout, stderr,
|
|
13
|
-
* and the parsed JSON report (or null if stdout is not valid JSON).
|
|
14
|
-
*/
|
|
15
|
-
function runValidator(wbPath, reqPath) {
|
|
16
|
-
const args = [VALIDATE, wbPath];
|
|
17
|
-
if (reqPath) args.push(reqPath);
|
|
18
|
-
const result = spawnSync('node', args, { encoding: 'utf8' });
|
|
19
|
-
let report = null;
|
|
20
|
-
try { report = JSON.parse(result.stdout); } catch (_) {}
|
|
21
|
-
return { exitCode: result.status, stdout: result.stdout, stderr: result.stderr, report };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Minimal valid work breakdown JSON that passes all checks 1–23 when no
|
|
26
|
-
* requirements file is provided. Used as a base for inline fixtures.
|
|
27
|
-
*/
|
|
28
|
-
const MINIMAL_VALID_WB = {
|
|
29
|
-
schemaVersion: 2,
|
|
30
|
-
phases: [
|
|
31
|
-
{
|
|
32
|
-
id: 'US-01',
|
|
33
|
-
type: 'user-story',
|
|
34
|
-
title: 'Test phase',
|
|
35
|
-
commit: 'feat(US-01): test',
|
|
36
|
-
tasks: [
|
|
37
|
-
{
|
|
38
|
-
id: 'US-01-TASK-BE-01',
|
|
39
|
-
title: 'A task',
|
|
40
|
-
outcome: 'Something is done',
|
|
41
|
-
domain: 'BE',
|
|
42
|
-
agentType: 'developer-backend',
|
|
43
|
-
dependsOn: [],
|
|
44
|
-
acceptanceCriteria: [],
|
|
45
|
-
verification: { commands: ['node --version'] },
|
|
46
|
-
estimate: { agentMinutes: 10, tokens: 5000 },
|
|
47
|
-
outputCount: 1,
|
|
48
|
-
groupingRationale: 'Single responsibility',
|
|
49
|
-
commit: { subject: 'feat(US-01): implement task' },
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// ── helper — write a WB object to a temp file and return the path ─────────────
|
|
57
|
-
|
|
58
|
-
function writeTmp(tmpFile, wb) {
|
|
59
|
-
fs.writeFileSync(tmpFile, JSON.stringify(wb));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
describe('wb-validate.js — checks 1–7', () => {
|
|
65
|
-
|
|
66
|
-
// ── Check 1: missing file argument ──────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
describe('check 1 — missing file argument', () => {
|
|
69
|
-
test('exits with code 2 and reports "missing required argument" to stderr when no path argument is given', () => {
|
|
70
|
-
// Arrange: invoke the validator with no arguments at all
|
|
71
|
-
// Act
|
|
72
|
-
const result = spawnSync('node', [VALIDATE], { encoding: 'utf8' });
|
|
73
|
-
// Assert
|
|
74
|
-
expect(result.status).toBe(2);
|
|
75
|
-
expect(result.stderr).toContain('missing required argument');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ── Check 2: schema version ──────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
describe('check 2 — schema version', () => {
|
|
82
|
-
let tmpFile;
|
|
83
|
-
|
|
84
|
-
beforeEach(() => {
|
|
85
|
-
tmpFile = path.join(os.tmpdir(), `wb-test-${Date.now()}.json`);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
afterEach(() => {
|
|
89
|
-
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('exits 0 and report.valid is true when schemaVersion is exactly 2', () => {
|
|
93
|
-
// Arrange
|
|
94
|
-
writeTmp(tmpFile, MINIMAL_VALID_WB);
|
|
95
|
-
// Act
|
|
96
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
97
|
-
// Assert
|
|
98
|
-
expect(exitCode).toBe(0);
|
|
99
|
-
expect(report.valid).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('exits 1, report.valid is false, and schema_version_invalid is reported when schemaVersion is 1', () => {
|
|
103
|
-
// Arrange
|
|
104
|
-
writeTmp(tmpFile, { ...MINIMAL_VALID_WB, schemaVersion: 1 });
|
|
105
|
-
// Act
|
|
106
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
107
|
-
// Assert
|
|
108
|
-
expect(exitCode).toBe(1);
|
|
109
|
-
expect(report.valid).toBe(false);
|
|
110
|
-
expect(report.errors.some(e => e.category === 'schema_version_invalid')).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('exits 1, report.valid is false, and schema_version_invalid is reported when schemaVersion is null', () => {
|
|
114
|
-
// Arrange
|
|
115
|
-
writeTmp(tmpFile, { ...MINIMAL_VALID_WB, schemaVersion: null });
|
|
116
|
-
// Act
|
|
117
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
118
|
-
// Assert
|
|
119
|
-
expect(exitCode).toBe(1);
|
|
120
|
-
expect(report.valid).toBe(false);
|
|
121
|
-
expect(report.errors.some(e => e.category === 'schema_version_invalid')).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// ── Check 3: unique task IDs ─────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
describe('check 3 — unique task IDs', () => {
|
|
128
|
-
test('exits 1, report.valid is false, and unique_id_violation is reported when the same task ID appears in multiple phases', () => {
|
|
129
|
-
// Arrange: fixture has INFRA-TASK-INFRA-01 in both the INFRA and US-01 phases
|
|
130
|
-
const wbPath = path.join(FIXTURES, 'wb-invalid-duplicate-id.json');
|
|
131
|
-
// Act
|
|
132
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
133
|
-
// Assert
|
|
134
|
-
expect(exitCode).toBe(1);
|
|
135
|
-
expect(report.valid).toBe(false);
|
|
136
|
-
expect(report.errors.some(e => e.category === 'unique_id_violation')).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('exits 0, report.valid is true, and no unique_id_violation errors are present for a valid work breakdown', () => {
|
|
140
|
-
// Arrange
|
|
141
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
142
|
-
// Act
|
|
143
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
144
|
-
// Assert
|
|
145
|
-
expect(exitCode).toBe(0);
|
|
146
|
-
expect(report.valid).toBe(true);
|
|
147
|
-
expect(report.errors.filter(e => e.category === 'unique_id_violation')).toHaveLength(0);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// ── Check 4: required fields ─────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
describe('check 4 — required fields', () => {
|
|
154
|
-
test('exits 1, report.valid is false, and missing_field with field "outcome" is reported when outcome is absent from a task', () => {
|
|
155
|
-
// Arrange: fixture has INFRA-TASK-INFRA-01 with the "outcome" field omitted
|
|
156
|
-
const wbPath = path.join(FIXTURES, 'wb-invalid-missing-field.json');
|
|
157
|
-
// Act
|
|
158
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
159
|
-
// Assert
|
|
160
|
-
expect(exitCode).toBe(1);
|
|
161
|
-
expect(report.valid).toBe(false);
|
|
162
|
-
const outcomeError = report.errors.find(
|
|
163
|
-
e => e.category === 'missing_field' && e.field === 'outcome',
|
|
164
|
-
);
|
|
165
|
-
expect(outcomeError).toBeDefined();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('exits 0, report.valid is true, and no missing_field errors are present for a valid work breakdown', () => {
|
|
169
|
-
// Arrange
|
|
170
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
171
|
-
// Act
|
|
172
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
173
|
-
// Assert
|
|
174
|
-
expect(exitCode).toBe(0);
|
|
175
|
-
expect(report.valid).toBe(true);
|
|
176
|
-
expect(report.errors.filter(e => e.category === 'missing_field')).toHaveLength(0);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// W3 regression guard — groupingRationale must be enforced by check 4
|
|
180
|
-
// Before fix: groupingRationale was in REQUIRED_TASK_FIELDS but absent from
|
|
181
|
-
// topLevelFields, so a task without the field passed check 4 with exit 0.
|
|
182
|
-
|
|
183
|
-
test('W3: exits 1 and missing_field with field "groupingRationale" is reported when groupingRationale is absent from a task', () => {
|
|
184
|
-
const { groupingRationale: _drop, ...taskWithoutRationale } = MINIMAL_VALID_WB.phases[0].tasks[0];
|
|
185
|
-
const wb = {
|
|
186
|
-
...MINIMAL_VALID_WB,
|
|
187
|
-
phases: [{ ...MINIMAL_VALID_WB.phases[0], tasks: [taskWithoutRationale] }],
|
|
188
|
-
};
|
|
189
|
-
const tmpPath = path.join(os.tmpdir(), `wb-w3-${Date.now()}.json`);
|
|
190
|
-
writeTmp(tmpPath, wb);
|
|
191
|
-
try {
|
|
192
|
-
const { exitCode, report } = runValidator(tmpPath);
|
|
193
|
-
expect(exitCode).toBe(1);
|
|
194
|
-
expect(report.valid).toBe(false);
|
|
195
|
-
const err = report.errors.find(e => e.category === 'missing_field' && e.field === 'groupingRationale');
|
|
196
|
-
expect(err).toBeDefined();
|
|
197
|
-
} finally {
|
|
198
|
-
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test('W3: exits 0 and no missing_field for groupingRationale when the field is present and non-empty', () => {
|
|
203
|
-
// MINIMAL_VALID_WB already includes groupingRationale — use it directly
|
|
204
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
205
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
206
|
-
expect(exitCode).toBe(0);
|
|
207
|
-
expect(report.errors.filter(e => e.category === 'missing_field' && e.field === 'groupingRationale')).toHaveLength(0);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// ── Check 5: task ID format ──────────────────────────────────────────────────
|
|
212
|
-
|
|
213
|
-
describe('check 5 — task ID format', () => {
|
|
214
|
-
let tmpFile;
|
|
215
|
-
|
|
216
|
-
beforeEach(() => {
|
|
217
|
-
tmpFile = path.join(os.tmpdir(), `wb-test-${Date.now()}.json`);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
afterEach(() => {
|
|
221
|
-
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('exits 1, report.valid is false, and invalid_id_format is reported when a non-INFRA task ID does not match the expected pattern', () => {
|
|
225
|
-
// Arrange: replace the task id with a string that matches no expected pattern
|
|
226
|
-
const wb = {
|
|
227
|
-
...MINIMAL_VALID_WB,
|
|
228
|
-
phases: [
|
|
229
|
-
{
|
|
230
|
-
...MINIMAL_VALID_WB.phases[0],
|
|
231
|
-
tasks: [
|
|
232
|
-
{ ...MINIMAL_VALID_WB.phases[0].tasks[0], id: 'BAD-FORMAT' },
|
|
233
|
-
],
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
};
|
|
237
|
-
writeTmp(tmpFile, wb);
|
|
238
|
-
// Act
|
|
239
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
240
|
-
// Assert
|
|
241
|
-
expect(exitCode).toBe(1);
|
|
242
|
-
expect(report.valid).toBe(false);
|
|
243
|
-
expect(report.errors.some(e => e.category === 'invalid_id_format')).toBe(true);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('exits 0, report.valid is true, and no invalid_id_format errors are present for a valid work breakdown', () => {
|
|
247
|
-
// Arrange
|
|
248
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
249
|
-
// Act
|
|
250
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
251
|
-
// Assert
|
|
252
|
-
expect(exitCode).toBe(0);
|
|
253
|
-
expect(report.valid).toBe(true);
|
|
254
|
-
expect(report.errors.filter(e => e.category === 'invalid_id_format')).toHaveLength(0);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ── Check 6: domain whitelist ────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
describe('check 6 — domain whitelist', () => {
|
|
261
|
-
test('exits 1, report.valid is false, and invalid_domain is reported when a task has an unrecognised domain value', () => {
|
|
262
|
-
// Arrange: fixture has domain "BACKEND" which is not in the VALID_DOMAINS list
|
|
263
|
-
const wbPath = path.join(FIXTURES, 'wb-invalid-bad-domain.json');
|
|
264
|
-
// Act
|
|
265
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
266
|
-
// Assert
|
|
267
|
-
expect(exitCode).toBe(1);
|
|
268
|
-
expect(report.valid).toBe(false);
|
|
269
|
-
expect(report.errors.some(e => e.category === 'invalid_domain')).toBe(true);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test('exits 0, report.valid is true, and no invalid_domain errors are present for a valid work breakdown', () => {
|
|
273
|
-
// Arrange
|
|
274
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
275
|
-
// Act
|
|
276
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
277
|
-
// Assert
|
|
278
|
-
expect(exitCode).toBe(0);
|
|
279
|
-
expect(report.valid).toBe(true);
|
|
280
|
-
expect(report.errors.filter(e => e.category === 'invalid_domain')).toHaveLength(0);
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// ── Check 7: agentType whitelist ─────────────────────────────────────────────
|
|
285
|
-
|
|
286
|
-
describe('check 7 — agentType whitelist', () => {
|
|
287
|
-
let tmpFile;
|
|
288
|
-
|
|
289
|
-
beforeEach(() => {
|
|
290
|
-
tmpFile = path.join(os.tmpdir(), `wb-test-${Date.now()}.json`);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
afterEach(() => {
|
|
294
|
-
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test('exits 1, report.valid is false, and invalid_agent_type is reported when a task has an unrecognised agentType', () => {
|
|
298
|
-
// Arrange: replace agentType with a value that is not in the VALID_AGENT_TYPES list
|
|
299
|
-
const wb = {
|
|
300
|
-
...MINIMAL_VALID_WB,
|
|
301
|
-
phases: [
|
|
302
|
-
{
|
|
303
|
-
...MINIMAL_VALID_WB.phases[0],
|
|
304
|
-
tasks: [
|
|
305
|
-
{ ...MINIMAL_VALID_WB.phases[0].tasks[0], agentType: 'unknown-agent' },
|
|
306
|
-
],
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
};
|
|
310
|
-
writeTmp(tmpFile, wb);
|
|
311
|
-
// Act
|
|
312
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
313
|
-
// Assert
|
|
314
|
-
expect(exitCode).toBe(1);
|
|
315
|
-
expect(report.valid).toBe(false);
|
|
316
|
-
expect(report.errors.some(e => e.category === 'invalid_agent_type')).toBe(true);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test('exits 0, report.valid is true, and no invalid_agent_type errors are present for a valid work breakdown', () => {
|
|
320
|
-
// Arrange
|
|
321
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
322
|
-
// Act
|
|
323
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
324
|
-
// Assert
|
|
325
|
-
expect(exitCode).toBe(0);
|
|
326
|
-
expect(report.valid).toBe(true);
|
|
327
|
-
expect(report.errors.filter(e => e.category === 'invalid_agent_type')).toHaveLength(0);
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
});
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const VALIDATE = path.resolve(__dirname, '../wb-validate.js');
|
|
9
|
-
const FIXTURES = path.resolve(__dirname, 'fixtures');
|
|
10
|
-
|
|
11
|
-
function runValidator(wbPath) {
|
|
12
|
-
const result = spawnSync('node', [VALIDATE, wbPath], { encoding: 'utf8' });
|
|
13
|
-
let report = null;
|
|
14
|
-
try { report = JSON.parse(result.stdout); } catch (_) {}
|
|
15
|
-
return { exitCode: result.status, report, stderr: result.stderr };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ── Shared temp-file lifecycle ────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
let tmpFile;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => { tmpFile = path.join(os.tmpdir(), `wb-t17-${Date.now()}.json`); });
|
|
23
|
-
afterEach(() => { try { fs.unlinkSync(tmpFile); } catch (_) {} });
|
|
24
|
-
|
|
25
|
-
function writeFixture(obj) { fs.writeFileSync(tmpFile, JSON.stringify(obj, null, 2)); return tmpFile; }
|
|
26
|
-
|
|
27
|
-
// ── Domain builders ───────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
function makeTask(id, dependsOn = []) {
|
|
30
|
-
return {
|
|
31
|
-
id,
|
|
32
|
-
title: `Task ${id}`,
|
|
33
|
-
outcome: 'Done',
|
|
34
|
-
domain: 'BE',
|
|
35
|
-
agentType: 'developer-backend',
|
|
36
|
-
dependsOn,
|
|
37
|
-
acceptanceCriteria: [],
|
|
38
|
-
verification: { commands: ['node --version'] },
|
|
39
|
-
estimate: { agentMinutes: 10, tokens: 5000 },
|
|
40
|
-
outputCount: 1,
|
|
41
|
-
groupingRationale: 'Single',
|
|
42
|
-
commit: { subject: `feat: ${id}` },
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makePhase(id, tasks) {
|
|
47
|
-
return { id, type: 'user-story', title: `Phase ${id}`, commit: `feat(${id}): done`, tasks };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeWB(phases) { return { schemaVersion: 2, phases }; }
|
|
51
|
-
|
|
52
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
describe('wb-validate.js — check 11 — task cycle detection (DFS gray/black coloring)', () => {
|
|
55
|
-
|
|
56
|
-
// ── Test A: existing cycle fixture ───────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
test('exits 1 and reports at least one task_cycle_detected error when the existing wb-invalid-cycle.json fixture is validated', () => {
|
|
59
|
-
// Arrange: use the pre-existing cycle fixture (INFRA-TASK-INFRA-01 ↔ INFRA-TASK-INFRA-02)
|
|
60
|
-
const wbPath = path.join(FIXTURES, 'wb-invalid-cycle.json');
|
|
61
|
-
|
|
62
|
-
// Act
|
|
63
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
64
|
-
|
|
65
|
-
// Assert
|
|
66
|
-
expect(exitCode).toBe(1);
|
|
67
|
-
expect(report.errors.some(e => e.category === 'task_cycle_detected')).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// ── Test B: two-node mutual cycle A→B, B→A ───────────────────────────────────
|
|
71
|
-
|
|
72
|
-
test('exits 1, reports task_cycle_detected, and details.cycleMembers contains both task IDs for a two-node cycle (A→B, B→A)', () => {
|
|
73
|
-
// Arrange
|
|
74
|
-
const wb = makeWB([makePhase('US-01', [
|
|
75
|
-
makeTask('US-01-TASK-BE-01', ['US-01-TASK-BE-02']),
|
|
76
|
-
makeTask('US-01-TASK-BE-02', ['US-01-TASK-BE-01']),
|
|
77
|
-
])]);
|
|
78
|
-
writeFixture(wb);
|
|
79
|
-
|
|
80
|
-
// Act
|
|
81
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
82
|
-
|
|
83
|
-
// Assert
|
|
84
|
-
expect(exitCode).toBe(1);
|
|
85
|
-
const cycleErr = report.errors.find(e => e.category === 'task_cycle_detected');
|
|
86
|
-
expect(cycleErr).toBeDefined();
|
|
87
|
-
expect(Array.isArray(cycleErr.details.cycleMembers)).toBe(true);
|
|
88
|
-
expect(cycleErr.details.cycleMembers).toContain('US-01-TASK-BE-01');
|
|
89
|
-
expect(cycleErr.details.cycleMembers).toContain('US-01-TASK-BE-02');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// ── Test C: three-node cycle A→B→C→A ────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
test('exits 1, reports task_cycle_detected, and details.cycleMembers has at least 3 elements for a three-node cycle (A→B→C→A)', () => {
|
|
95
|
-
// Arrange
|
|
96
|
-
const wb = makeWB([makePhase('US-01', [
|
|
97
|
-
makeTask('US-01-TASK-BE-01', ['US-01-TASK-BE-02']),
|
|
98
|
-
makeTask('US-01-TASK-BE-02', ['US-01-TASK-BE-03']),
|
|
99
|
-
makeTask('US-01-TASK-BE-03', ['US-01-TASK-BE-01']),
|
|
100
|
-
])]);
|
|
101
|
-
writeFixture(wb);
|
|
102
|
-
|
|
103
|
-
// Act
|
|
104
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
105
|
-
|
|
106
|
-
// Assert
|
|
107
|
-
expect(exitCode).toBe(1);
|
|
108
|
-
const cycleErr = report.errors.find(e => e.category === 'task_cycle_detected');
|
|
109
|
-
expect(cycleErr).toBeDefined();
|
|
110
|
-
expect(Array.isArray(cycleErr.details.cycleMembers)).toBe(true);
|
|
111
|
-
expect(cycleErr.details.cycleMembers.length).toBeGreaterThanOrEqual(3);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// ── Test D: acyclic diamond graph — no cycles ────────────────────────────────
|
|
115
|
-
|
|
116
|
-
test('exits 0, reports no task_cycle_detected errors, and report.dependencies.taskCycles is an empty array for an acyclic diamond graph (A→B, A→C, B→D, C→D)', () => {
|
|
117
|
-
// Arrange
|
|
118
|
-
const wb = makeWB([makePhase('US-01', [
|
|
119
|
-
makeTask('US-01-TASK-BE-01', []),
|
|
120
|
-
makeTask('US-01-TASK-BE-02', ['US-01-TASK-BE-01']),
|
|
121
|
-
makeTask('US-01-TASK-BE-03', ['US-01-TASK-BE-01']),
|
|
122
|
-
makeTask('US-01-TASK-BE-04', ['US-01-TASK-BE-02', 'US-01-TASK-BE-03']),
|
|
123
|
-
])]);
|
|
124
|
-
writeFixture(wb);
|
|
125
|
-
|
|
126
|
-
// Act
|
|
127
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
128
|
-
|
|
129
|
-
// Assert
|
|
130
|
-
expect(exitCode).toBe(0);
|
|
131
|
-
expect(report.errors.filter(e => e.category === 'task_cycle_detected')).toHaveLength(0);
|
|
132
|
-
expect(report.dependencies.taskCycles).toEqual([]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ── Test E: cross-phase cycle ────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
test('exits 1 and reports task_cycle_detected when a mutual dependency cycle spans two phases (US-01-A→US-02-B, US-02-B→US-01-A)', () => {
|
|
138
|
-
// Arrange: task in US-01 depends on task in US-02, which depends back on US-01 task
|
|
139
|
-
const wb = makeWB([
|
|
140
|
-
makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['US-02-TASK-BE-01'])]),
|
|
141
|
-
makePhase('US-02', [makeTask('US-02-TASK-BE-01', ['US-01-TASK-BE-01'])]),
|
|
142
|
-
]);
|
|
143
|
-
writeFixture(wb);
|
|
144
|
-
|
|
145
|
-
// Act
|
|
146
|
-
const { exitCode, report } = runValidator(tmpFile);
|
|
147
|
-
|
|
148
|
-
// Assert
|
|
149
|
-
expect(exitCode).toBe(1);
|
|
150
|
-
expect(report.errors.some(e => e.category === 'task_cycle_detected')).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ── Test F: taskCycles always present in report ──────────────────────────────
|
|
154
|
-
|
|
155
|
-
test('report.dependencies.taskCycles is always an array even when there are no cycles (valid fixture exits 0)', () => {
|
|
156
|
-
// Arrange: use the pre-existing valid fixture which has no dependency cycles
|
|
157
|
-
const wbPath = path.join(FIXTURES, 'wb-valid.json');
|
|
158
|
-
|
|
159
|
-
// Act
|
|
160
|
-
const { exitCode, report } = runValidator(wbPath);
|
|
161
|
-
|
|
162
|
-
// Assert
|
|
163
|
-
expect(exitCode).toBe(0);
|
|
164
|
-
expect(Array.isArray(report.dependencies.taskCycles)).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
});
|