@nathapp/nax 0.19.0 → 0.21.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/settings.json +15 -0
- package/docs/20260304-review-nax.md +492 -0
- package/docs/ROADMAP.md +52 -18
- package/docs/specs/bug-039-orphan-processes.md +131 -0
- package/docs/specs/bug-040-review-rectification.md +82 -0
- package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
- package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
- package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
- package/docs/specs/feat-011-file-context-strategy.md +73 -0
- package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
- package/docs/specs/feat-013-test-after-review.md +89 -0
- package/docs/specs/feat-014-heartbeat-observability.md +127 -0
- package/memory/topic/feat-010-baseref.md +28 -0
- package/memory/topic/feat-013-test-after-deprecation.md +22 -0
- package/nax/config.json +7 -4
- package/nax/features/bug-039-medium/prd.json +45 -0
- package/nax/features/verify-v2/prd.json +79 -0
- package/nax/features/verify-v2/progress.txt +3 -0
- package/package.json +2 -2
- package/src/agents/claude.ts +66 -7
- package/src/config/defaults.ts +2 -1
- package/src/config/schemas.ts +2 -0
- package/src/config/types.ts +4 -0
- package/src/context/builder.ts +9 -1
- package/src/execution/lifecycle/index.ts +1 -0
- package/src/execution/lifecycle/run-completion.ts +29 -0
- package/src/execution/lifecycle/run-regression.ts +301 -0
- package/src/execution/pipeline-result-handler.ts +0 -1
- package/src/execution/post-verify.ts +31 -194
- package/src/execution/runner.ts +1 -0
- package/src/execution/sequential-executor.ts +1 -0
- package/src/pipeline/stages/verify.ts +27 -23
- package/src/pipeline/types.ts +2 -0
- package/src/review/runner.ts +39 -4
- package/src/routing/router.ts +3 -3
- package/src/routing/strategies/keyword.ts +5 -2
- package/src/routing/strategies/llm.ts +27 -1
- package/src/utils/git.ts +49 -25
- package/src/verification/executor.ts +8 -2
- package/src/verification/smart-runner.ts +58 -10
- package/test/integration/plugin-routing.test.ts +1 -1
- package/test/integration/rectification-flow.test.ts +3 -3
- package/test/integration/review-config-commands.test.ts +1 -1
- package/test/integration/verify-stage.test.ts +9 -0
- package/test/unit/agents/claude.test.ts +106 -0
- package/test/unit/config/defaults.test.ts +69 -0
- package/test/unit/config/regression-gate-schema.test.ts +159 -0
- package/test/unit/context.test.ts +6 -3
- package/test/unit/execution/lifecycle/run-completion.test.ts +239 -0
- package/test/unit/execution/lifecycle/run-regression.test.ts +418 -0
- package/test/unit/execution/post-verify-regression.test.ts +31 -84
- package/test/unit/execution/post-verify.test.ts +28 -48
- package/test/unit/pipeline/stages/verify.test.ts +266 -0
- package/test/unit/pipeline/verify-smart-runner.test.ts +2 -1
- package/test/unit/prd-auto-default.test.ts +2 -2
- package/test/unit/routing/routing-stability.test.ts +1 -1
- package/test/unit/routing/strategies/llm.test.ts +250 -0
- package/test/unit/routing.test.ts +7 -7
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Runner Reverse Mapping Tests + Deferred Regression Gate Tests
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - reverseMapTestToSource: maps test files back to source files
|
|
6
|
+
* - runDeferredRegression: deferred regression gate behavior
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
import type { NaxConfig } from "../../../../src/config";
|
|
11
|
+
import type { PRD, UserStory } from "../../../../src/prd";
|
|
12
|
+
import { DEFAULT_CONFIG } from "../../../../src/config/defaults";
|
|
13
|
+
import {
|
|
14
|
+
_regressionDeps,
|
|
15
|
+
runDeferredRegression,
|
|
16
|
+
} from "../../../../src/execution/lifecycle/run-regression";
|
|
17
|
+
import type { VerificationResult } from "../../../../src/verification";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Test helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function makeStory(id: string, status: UserStory["status"]): UserStory {
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
title: `Story ${id}`,
|
|
27
|
+
description: "Test story",
|
|
28
|
+
acceptanceCriteria: [],
|
|
29
|
+
tags: [],
|
|
30
|
+
dependencies: [],
|
|
31
|
+
status,
|
|
32
|
+
passes: status === "passed",
|
|
33
|
+
escalations: [],
|
|
34
|
+
attempts: 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makePRD(stories: Array<{ id: string; status: UserStory["status"] }>): PRD {
|
|
39
|
+
return {
|
|
40
|
+
project: "test-project",
|
|
41
|
+
feature: "test-feature",
|
|
42
|
+
branchName: "test-branch",
|
|
43
|
+
createdAt: new Date().toISOString(),
|
|
44
|
+
updatedAt: new Date().toISOString(),
|
|
45
|
+
userStories: stories.map(({ id, status }) => makeStory(id, status)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeConfig(
|
|
50
|
+
regressionMode?: "deferred" | "per-story" | "disabled",
|
|
51
|
+
testCommand?: string,
|
|
52
|
+
): NaxConfig {
|
|
53
|
+
return {
|
|
54
|
+
...DEFAULT_CONFIG,
|
|
55
|
+
execution: {
|
|
56
|
+
...DEFAULT_CONFIG.execution,
|
|
57
|
+
regressionGate: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
timeoutSeconds: 30,
|
|
60
|
+
acceptOnTimeout: true,
|
|
61
|
+
...(regressionMode !== undefined ? { mode: regressionMode } : {}),
|
|
62
|
+
maxRectificationAttempts: 2,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
quality: {
|
|
66
|
+
...DEFAULT_CONFIG.quality,
|
|
67
|
+
commands: {
|
|
68
|
+
...(testCommand ? { test: testCommand } : {}),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("reverseMapTestToSource", () => {
|
|
75
|
+
test("should map test/unit files to source files", async () => {
|
|
76
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
77
|
+
|
|
78
|
+
const testFiles = ["/repo/test/unit/foo/bar.test.ts"];
|
|
79
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
80
|
+
|
|
81
|
+
expect(result).toEqual(["src/foo/bar.ts"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("should map test/integration files to source files", async () => {
|
|
85
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
86
|
+
|
|
87
|
+
const testFiles = ["/repo/test/integration/foo/bar.test.ts"];
|
|
88
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual(["src/foo/bar.ts"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("should ignore non-test files", async () => {
|
|
94
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
95
|
+
|
|
96
|
+
const testFiles = ["/repo/src/foo/bar.ts"];
|
|
97
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("should deduplicate results", async () => {
|
|
103
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
104
|
+
|
|
105
|
+
const testFiles = ["/repo/test/unit/foo/bar.test.ts", "/repo/test/integration/foo/bar.test.ts"];
|
|
106
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
107
|
+
|
|
108
|
+
expect(result).toEqual(["src/foo/bar.ts"]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("should handle paths without leading workdir", async () => {
|
|
112
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
113
|
+
|
|
114
|
+
const testFiles = ["test/unit/foo/bar.test.ts"];
|
|
115
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual(["src/foo/bar.ts"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("should preserve order when mapping multiple files", async () => {
|
|
121
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
122
|
+
|
|
123
|
+
const testFiles = [
|
|
124
|
+
"/repo/test/unit/aaa.test.ts",
|
|
125
|
+
"/repo/test/unit/bbb.test.ts",
|
|
126
|
+
"/repo/test/unit/ccc.test.ts",
|
|
127
|
+
];
|
|
128
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
129
|
+
|
|
130
|
+
expect(result).toEqual(["src/aaa.ts", "src/bbb.ts", "src/ccc.ts"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("should handle empty input", async () => {
|
|
134
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
135
|
+
|
|
136
|
+
const testFiles: string[] = [];
|
|
137
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
138
|
+
|
|
139
|
+
expect(result).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("should filter out files with .test.js extension", async () => {
|
|
143
|
+
const { reverseMapTestToSource } = await import("../../../../src/verification/smart-runner");
|
|
144
|
+
|
|
145
|
+
// Only .test.ts files should be mapped (not .test.js)
|
|
146
|
+
const testFiles = ["/repo/test/unit/foo.js"];
|
|
147
|
+
const result = reverseMapTestToSource(testFiles, "/repo");
|
|
148
|
+
|
|
149
|
+
expect(result).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// runDeferredRegression
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
describe("runDeferredRegression", () => {
|
|
158
|
+
test("returns success immediately when mode is 'disabled'", async () => {
|
|
159
|
+
const { runDeferredRegression } = await import(
|
|
160
|
+
"../../../../src/execution/lifecycle/run-regression"
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const result = await runDeferredRegression({
|
|
164
|
+
config: makeConfig("disabled", "bun test"),
|
|
165
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
166
|
+
workdir: "/tmp/nax-test-disabled",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
expect(result.failedTests).toBe(0);
|
|
171
|
+
expect(result.rectificationAttempts).toBe(0);
|
|
172
|
+
expect(result.affectedStories).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns success immediately when mode is 'per-story' (deferred not applicable)", async () => {
|
|
176
|
+
const { runDeferredRegression } = await import(
|
|
177
|
+
"../../../../src/execution/lifecycle/run-regression"
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const result = await runDeferredRegression({
|
|
181
|
+
config: makeConfig("per-story", "bun test"),
|
|
182
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
183
|
+
workdir: "/tmp/nax-test-per-story",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
expect(result.rectificationAttempts).toBe(0);
|
|
188
|
+
expect(result.affectedStories).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("returns success when no passed stories exist (partial completion)", async () => {
|
|
192
|
+
const { runDeferredRegression } = await import(
|
|
193
|
+
"../../../../src/execution/lifecycle/run-regression"
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const result = await runDeferredRegression({
|
|
197
|
+
config: makeConfig("deferred", "bun test"),
|
|
198
|
+
prd: makePRD([
|
|
199
|
+
{ id: "US-001", status: "pending" },
|
|
200
|
+
{ id: "US-002", status: "failed" },
|
|
201
|
+
]),
|
|
202
|
+
workdir: "/tmp/nax-test-no-passed",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.success).toBe(true);
|
|
206
|
+
expect(result.passedTests).toBe(0);
|
|
207
|
+
expect(result.failedTests).toBe(0);
|
|
208
|
+
expect(result.affectedStories).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("result shape has all required fields", async () => {
|
|
212
|
+
const { runDeferredRegression } = await import(
|
|
213
|
+
"../../../../src/execution/lifecycle/run-regression"
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const result = await runDeferredRegression({
|
|
217
|
+
config: makeConfig("disabled", "bun test"),
|
|
218
|
+
prd: makePRD([]),
|
|
219
|
+
workdir: "/tmp/nax-test-shape",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(typeof result.success).toBe("boolean");
|
|
223
|
+
expect(typeof result.failedTests).toBe("number");
|
|
224
|
+
expect(typeof result.passedTests).toBe("number");
|
|
225
|
+
expect(typeof result.rectificationAttempts).toBe("number");
|
|
226
|
+
expect(Array.isArray(result.affectedStories)).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("affectedStories contains only string values", async () => {
|
|
230
|
+
const { runDeferredRegression } = await import(
|
|
231
|
+
"../../../../src/execution/lifecycle/run-regression"
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const result = await runDeferredRegression({
|
|
235
|
+
config: makeConfig("disabled", "bun test"),
|
|
236
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
237
|
+
workdir: "/tmp/nax-test-story-ids",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
for (const storyId of result.affectedStories) {
|
|
241
|
+
expect(typeof storyId).toBe("string");
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("passedTests is non-negative integer", async () => {
|
|
246
|
+
const { runDeferredRegression } = await import(
|
|
247
|
+
"../../../../src/execution/lifecycle/run-regression"
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const result = await runDeferredRegression({
|
|
251
|
+
config: makeConfig("disabled", "bun test"),
|
|
252
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
253
|
+
workdir: "/tmp/nax-test-counts",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(result.passedTests).toBeGreaterThanOrEqual(0);
|
|
257
|
+
expect(Number.isInteger(result.passedTests)).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// runDeferredRegression — behavioral tests with mocked _regressionDeps
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
const origRegressionDeps = {
|
|
266
|
+
runVerification: _regressionDeps.runVerification,
|
|
267
|
+
runRectificationLoop: _regressionDeps.runRectificationLoop,
|
|
268
|
+
parseBunTestOutput: _regressionDeps.parseBunTestOutput,
|
|
269
|
+
reverseMapTestToSource: _regressionDeps.reverseMapTestToSource,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
// Reset mocks to identity (no-op) before each behavioral test
|
|
274
|
+
// Specific tests override as needed
|
|
275
|
+
_regressionDeps.runRectificationLoop = mock(async () => false);
|
|
276
|
+
_regressionDeps.reverseMapTestToSource = mock(() => []);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
afterEach(() => {
|
|
280
|
+
Object.assign(_regressionDeps, origRegressionDeps);
|
|
281
|
+
mock.restore();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("runDeferredRegression - behavioral tests (with mocked deps)", () => {
|
|
285
|
+
test("full suite passes → success with 0 rectification attempts", async () => {
|
|
286
|
+
_regressionDeps.runVerification = mock(async (): Promise<VerificationResult> => ({
|
|
287
|
+
status: "SUCCESS",
|
|
288
|
+
success: true,
|
|
289
|
+
countsTowardEscalation: true,
|
|
290
|
+
passCount: 42,
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
const result = await runDeferredRegression({
|
|
294
|
+
config: makeConfig("deferred", "bun test"),
|
|
295
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
296
|
+
workdir: "/tmp/nax-test-behavioral",
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(result.success).toBe(true);
|
|
300
|
+
expect(result.passedTests).toBe(42);
|
|
301
|
+
expect(result.rectificationAttempts).toBe(0);
|
|
302
|
+
expect(result.affectedStories).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("TIMEOUT + acceptOnTimeout=true → success", async () => {
|
|
306
|
+
_regressionDeps.runVerification = mock(async (): Promise<VerificationResult> => ({
|
|
307
|
+
status: "TIMEOUT",
|
|
308
|
+
success: false,
|
|
309
|
+
countsTowardEscalation: false,
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
const config = makeConfig("deferred", "bun test");
|
|
313
|
+
// acceptOnTimeout=true is already default in makeConfig
|
|
314
|
+
const result = await runDeferredRegression({
|
|
315
|
+
config,
|
|
316
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
317
|
+
workdir: "/tmp/nax-test-timeout-accept",
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(result.success).toBe(true);
|
|
321
|
+
expect(result.rectificationAttempts).toBe(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("TIMEOUT + acceptOnTimeout=false → failure", async () => {
|
|
325
|
+
_regressionDeps.runVerification = mock(async (): Promise<VerificationResult> => ({
|
|
326
|
+
status: "TIMEOUT",
|
|
327
|
+
success: false,
|
|
328
|
+
countsTowardEscalation: false,
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
const config: NaxConfig = {
|
|
332
|
+
...makeConfig("deferred", "bun test"),
|
|
333
|
+
execution: {
|
|
334
|
+
...makeConfig("deferred", "bun test").execution,
|
|
335
|
+
regressionGate: {
|
|
336
|
+
enabled: true,
|
|
337
|
+
timeoutSeconds: 30,
|
|
338
|
+
acceptOnTimeout: false,
|
|
339
|
+
mode: "deferred",
|
|
340
|
+
maxRectificationAttempts: 2,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const result = await runDeferredRegression({
|
|
346
|
+
config,
|
|
347
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
348
|
+
workdir: "/tmp/nax-test-timeout-reject",
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(result.success).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("full suite fails with no output → failure immediately (no rectification)", async () => {
|
|
355
|
+
_regressionDeps.runVerification = mock(async (): Promise<VerificationResult> => ({
|
|
356
|
+
status: "TEST_FAILURE",
|
|
357
|
+
success: false,
|
|
358
|
+
countsTowardEscalation: true,
|
|
359
|
+
failCount: 3,
|
|
360
|
+
// no output field
|
|
361
|
+
}));
|
|
362
|
+
|
|
363
|
+
const result = await runDeferredRegression({
|
|
364
|
+
config: makeConfig("deferred", "bun test"),
|
|
365
|
+
prd: makePRD([{ id: "US-001", status: "passed" }]),
|
|
366
|
+
workdir: "/tmp/nax-test-no-output",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(result.success).toBe(false);
|
|
370
|
+
expect(result.rectificationAttempts).toBe(0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("unmapped failures (no file field) → all passed stories in affectedStories", async () => {
|
|
374
|
+
let verCallCount = 0;
|
|
375
|
+
_regressionDeps.runVerification = mock(async (): Promise<VerificationResult> => {
|
|
376
|
+
verCallCount++;
|
|
377
|
+
if (verCallCount === 1) {
|
|
378
|
+
return {
|
|
379
|
+
status: "TEST_FAILURE",
|
|
380
|
+
success: false,
|
|
381
|
+
countsTowardEscalation: true,
|
|
382
|
+
output: "FAIL: some test\nerror: boom",
|
|
383
|
+
failCount: 1,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// second call (after rectification) still fails
|
|
387
|
+
return {
|
|
388
|
+
status: "TEST_FAILURE",
|
|
389
|
+
success: false,
|
|
390
|
+
countsTowardEscalation: true,
|
|
391
|
+
failCount: 1,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// parseBunTestOutput returns failures WITHOUT file property (unmapped)
|
|
396
|
+
// Cast via unknown to satisfy TestFailure type while testing the undefined case
|
|
397
|
+
_regressionDeps.parseBunTestOutput = mock(() => ({
|
|
398
|
+
failed: 1,
|
|
399
|
+
passed: 5,
|
|
400
|
+
failures: [{ testName: "some test", error: "boom" }],
|
|
401
|
+
})) as unknown as typeof _regressionDeps.parseBunTestOutput;
|
|
402
|
+
|
|
403
|
+
_regressionDeps.runRectificationLoop = mock(async () => false);
|
|
404
|
+
|
|
405
|
+
const result = await runDeferredRegression({
|
|
406
|
+
config: makeConfig("deferred", "bun test"),
|
|
407
|
+
prd: makePRD([
|
|
408
|
+
{ id: "US-001", status: "passed" },
|
|
409
|
+
{ id: "US-002", status: "passed" },
|
|
410
|
+
]),
|
|
411
|
+
workdir: "/tmp/nax-test-unmapped",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Both passed stories should be marked as affected (unmapped case)
|
|
415
|
+
expect(result.affectedStories).toContain("US-001");
|
|
416
|
+
expect(result.affectedStories).toContain("US-002");
|
|
417
|
+
});
|
|
418
|
+
});
|