@nathapp/nax 0.18.3 → 0.18.4
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/rules/01-project-conventions.md +34 -0
- package/.claude/rules/02-test-architecture.md +39 -0
- package/.claude/rules/03-test-writing.md +58 -0
- package/.claude/rules/04-forbidden-patterns.md +29 -0
- package/.githooks/pre-commit +13 -0
- package/CHANGELOG.md +9 -0
- package/CLAUDE.md +45 -122
- package/docker-compose.test.yml +1 -3
- package/docs/ROADMAP.md +9 -27
- package/package.json +1 -1
- package/src/config/schemas.ts +2 -0
- package/src/config/types.ts +5 -1
- package/src/execution/post-verify.ts +30 -12
- package/src/pipeline/stages/execution.ts +10 -2
- package/src/pipeline/stages/routing.ts +18 -4
- package/src/pipeline/stages/verify.ts +8 -1
- package/src/routing/strategies/keyword.ts +7 -4
- package/src/routing/strategies/llm.ts +40 -4
- package/test/{US-002-orchestrator.test.ts → integration/precheck-orchestrator.test.ts} +3 -3
- package/test/{execution/post-verify-bug026.test.ts → unit/execution/post-verify-regression.test.ts} +22 -50
- package/test/{execution → unit/execution}/post-verify.test.ts +1 -1
- package/test/unit/pipeline/routing-partial-override.test.ts +15 -36
- package/test/unit/pipeline/verify-smart-runner.test.ts +5 -6
- package/test/unit/routing/routing-stability.test.ts +207 -0
- package/test/unit/storyid-events.test.ts +20 -32
- package/test/unit/verification/smart-runner-config.test.ts +162 -0
- package/test/unit/{smart-test-runner.test.ts → verification/smart-runner-discovery.test.ts} +5 -164
- package/test/TEST_COVERAGE_US001.md +0 -217
- package/test/TEST_COVERAGE_US003.md +0 -84
- package/test/TEST_COVERAGE_US005.md +0 -86
|
@@ -85,7 +85,7 @@ export const executionStage: PipelineStage = {
|
|
|
85
85
|
const logger = getLogger();
|
|
86
86
|
|
|
87
87
|
// HARD FAILURE: No agent available — cannot proceed without an agent
|
|
88
|
-
const agent = getAgent(ctx.config.autoMode.defaultAgent);
|
|
88
|
+
const agent = _executionDeps.getAgent(ctx.config.autoMode.defaultAgent);
|
|
89
89
|
if (!agent) {
|
|
90
90
|
return {
|
|
91
91
|
action: "fail",
|
|
@@ -152,7 +152,7 @@ export const executionStage: PipelineStage = {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// Validate agent supports the requested tier
|
|
155
|
-
if (!validateAgentForTier(agent, ctx.routing.modelTier)) {
|
|
155
|
+
if (!_executionDeps.validateAgentForTier(agent, ctx.routing.modelTier)) {
|
|
156
156
|
logger.warn("execution", "Agent tier mismatch", {
|
|
157
157
|
storyId: ctx.story.id,
|
|
158
158
|
agentName: agent.name,
|
|
@@ -192,3 +192,11 @@ export const executionStage: PipelineStage = {
|
|
|
192
192
|
return { action: "continue" };
|
|
193
193
|
},
|
|
194
194
|
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
198
|
+
*/
|
|
199
|
+
export const _executionDeps = {
|
|
200
|
+
getAgent,
|
|
201
|
+
validateAgentForTier,
|
|
202
|
+
};
|
|
@@ -35,7 +35,7 @@ export const routingStage: PipelineStage = {
|
|
|
35
35
|
let routing: { complexity: string; testStrategy: string; modelTier: string; reasoning?: string };
|
|
36
36
|
if (ctx.story.routing) {
|
|
37
37
|
// Use cached complexity/testStrategy/modelTier
|
|
38
|
-
routing = await routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
38
|
+
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
39
39
|
// Override with cached values only when they are actually set
|
|
40
40
|
if (ctx.story.routing?.complexity) routing.complexity = ctx.story.routing.complexity;
|
|
41
41
|
if (ctx.story.routing?.testStrategy) routing.testStrategy = ctx.story.routing.testStrategy;
|
|
@@ -44,17 +44,20 @@ export const routingStage: PipelineStage = {
|
|
|
44
44
|
if (ctx.story.routing?.modelTier) {
|
|
45
45
|
routing.modelTier = ctx.story.routing.modelTier;
|
|
46
46
|
} else {
|
|
47
|
-
routing.modelTier = complexityToModelTier(
|
|
47
|
+
routing.modelTier = _routingDeps.complexityToModelTier(
|
|
48
|
+
routing.complexity as import("../../config").Complexity,
|
|
49
|
+
ctx.config,
|
|
50
|
+
);
|
|
48
51
|
}
|
|
49
52
|
} else {
|
|
50
53
|
// Fresh classification
|
|
51
|
-
routing = await routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
54
|
+
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
// BUG-010: Greenfield detection — force test-after if no test files exist
|
|
55
58
|
const greenfieldDetectionEnabled = ctx.config.tdd.greenfieldDetection ?? true;
|
|
56
59
|
if (greenfieldDetectionEnabled && routing.testStrategy.startsWith("three-session-tdd")) {
|
|
57
|
-
const isGreenfield = await isGreenfieldStory(ctx.story, ctx.workdir);
|
|
60
|
+
const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, ctx.workdir);
|
|
58
61
|
if (isGreenfield) {
|
|
59
62
|
logger.info("routing", "Greenfield detected — forcing test-after strategy", {
|
|
60
63
|
storyId: ctx.story.id,
|
|
@@ -84,3 +87,14 @@ export const routingStage: PipelineStage = {
|
|
|
84
87
|
return { action: "continue" };
|
|
85
88
|
},
|
|
86
89
|
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
93
|
+
* Tests can override individual functions without poisoning the module registry.
|
|
94
|
+
*/
|
|
95
|
+
export const _routingDeps = {
|
|
96
|
+
routeStory,
|
|
97
|
+
complexityToModelTier,
|
|
98
|
+
isGreenfieldStory,
|
|
99
|
+
clearCache,
|
|
100
|
+
};
|
|
@@ -99,7 +99,7 @@ export const verifyStage: PipelineStage = {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Use unified regression gate (includes 2s wait for agent process cleanup)
|
|
102
|
-
const result = await regression({
|
|
102
|
+
const result = await _verifyDeps.regression({
|
|
103
103
|
workdir: ctx.workdir,
|
|
104
104
|
command: effectiveCommand,
|
|
105
105
|
timeoutSeconds: ctx.config.execution.verificationTimeoutSeconds,
|
|
@@ -151,3 +151,10 @@ export const verifyStage: PipelineStage = {
|
|
|
151
151
|
return { action: "continue" };
|
|
152
152
|
},
|
|
153
153
|
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
157
|
+
*/
|
|
158
|
+
export const _verifyDeps = {
|
|
159
|
+
regression,
|
|
160
|
+
};
|
|
@@ -71,11 +71,13 @@ const PUBLIC_API_KEYWORDS = [
|
|
|
71
71
|
*/
|
|
72
72
|
function classifyComplexity(
|
|
73
73
|
title: string,
|
|
74
|
-
|
|
74
|
+
_description: string,
|
|
75
75
|
acceptanceCriteria: string[],
|
|
76
76
|
tags: string[] = [],
|
|
77
77
|
): Complexity {
|
|
78
|
-
|
|
78
|
+
// BUG-031: Exclude description — it accumulates priorErrors across retries and
|
|
79
|
+
// causes classification drift. Only use stable, immutable story fields.
|
|
80
|
+
const text = [title, ...acceptanceCriteria, ...tags].join(" ").toLowerCase();
|
|
79
81
|
|
|
80
82
|
if (EXPERT_KEYWORDS.some((kw) => text.includes(kw))) {
|
|
81
83
|
return "expert";
|
|
@@ -98,10 +100,11 @@ function classifyComplexity(
|
|
|
98
100
|
function determineTestStrategy(
|
|
99
101
|
complexity: Complexity,
|
|
100
102
|
title: string,
|
|
101
|
-
|
|
103
|
+
_description: string,
|
|
102
104
|
tags: string[] = [],
|
|
103
105
|
): TestStrategy {
|
|
104
|
-
|
|
106
|
+
// BUG-031: Exclude description — only use stable, immutable story fields.
|
|
107
|
+
const text = [title, ...tags].join(" ").toLowerCase();
|
|
105
108
|
|
|
106
109
|
const isSecurityCritical = SECURITY_KEYWORDS.some((kw) => text.includes(kw));
|
|
107
110
|
const isPublicApi = PUBLIC_API_KEYWORDS.some((kw) => text.includes(kw));
|
|
@@ -58,10 +58,7 @@ function evictOldest(): void {
|
|
|
58
58
|
* @returns LLM response text
|
|
59
59
|
* @throws Error on timeout or spawn failure
|
|
60
60
|
*/
|
|
61
|
-
async function
|
|
62
|
-
const llmConfig = config.routing.llm;
|
|
63
|
-
const timeoutMs = llmConfig?.timeoutMs ?? 15000;
|
|
64
|
-
|
|
61
|
+
async function callLlmOnce(modelTier: string, prompt: string, config: NaxConfig, timeoutMs: number): Promise<string> {
|
|
65
62
|
// Resolve model tier to actual model identifier
|
|
66
63
|
const modelEntry = config.models[modelTier];
|
|
67
64
|
if (!modelEntry) {
|
|
@@ -108,6 +105,45 @@ async function callLlm(modelTier: string, prompt: string, config: NaxConfig): Pr
|
|
|
108
105
|
}
|
|
109
106
|
}
|
|
110
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Call LLM via claude CLI with timeout and retry (BUG-033).
|
|
110
|
+
*
|
|
111
|
+
* @param modelTier - Model tier to use for routing call
|
|
112
|
+
* @param prompt - Prompt to send to LLM
|
|
113
|
+
* @param config - nax configuration
|
|
114
|
+
* @returns LLM response text
|
|
115
|
+
* @throws Error after all retries exhausted
|
|
116
|
+
*/
|
|
117
|
+
async function callLlm(modelTier: string, prompt: string, config: NaxConfig): Promise<string> {
|
|
118
|
+
const llmConfig = config.routing.llm;
|
|
119
|
+
const timeoutMs = llmConfig?.timeoutMs ?? 30000;
|
|
120
|
+
const maxRetries = llmConfig?.retries ?? 1;
|
|
121
|
+
const retryDelayMs = llmConfig?.retryDelayMs ?? 1000;
|
|
122
|
+
|
|
123
|
+
let lastError: Error | undefined;
|
|
124
|
+
|
|
125
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
126
|
+
try {
|
|
127
|
+
return await callLlmOnce(modelTier, prompt, config, timeoutMs);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
lastError = err as Error;
|
|
130
|
+
if (attempt < maxRetries) {
|
|
131
|
+
const logger = getLogger();
|
|
132
|
+
logger.warn(
|
|
133
|
+
"routing",
|
|
134
|
+
`LLM call failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${retryDelayMs}ms`,
|
|
135
|
+
{
|
|
136
|
+
error: lastError.message,
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
await Bun.sleep(retryDelayMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw lastError ?? new Error("LLM call failed with unknown error");
|
|
145
|
+
}
|
|
146
|
+
|
|
111
147
|
/**
|
|
112
148
|
* Route multiple stories in a single batch LLM call.
|
|
113
149
|
*
|
|
@@ -14,9 +14,9 @@ const skipInCI = process.env.CI ? test.skip : test;
|
|
|
14
14
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import type { NaxConfig } from "
|
|
18
|
-
import type { PRD } from "
|
|
19
|
-
import { EXIT_CODES, runPrecheck } from "
|
|
17
|
+
import type { NaxConfig } from "../../src/config";
|
|
18
|
+
import type { PRD } from "../../src/prd/types";
|
|
19
|
+
import { EXIT_CODES, runPrecheck } from "../../src/precheck";
|
|
20
20
|
|
|
21
21
|
// Helper to create a minimal valid git environment
|
|
22
22
|
async function setupGitRepo(dir: string): Promise<void> {
|
package/test/{execution/post-verify-bug026.test.ts → unit/execution/post-verify-regression.test.ts}
RENAMED
|
@@ -15,10 +15,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
15
15
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
|
-
import type { NaxConfig } from "
|
|
19
|
-
import type { PRD, UserStory } from "
|
|
20
|
-
import type { StoryMetrics } from "
|
|
21
|
-
import type { VerificationResult } from "
|
|
18
|
+
import type { NaxConfig } from "../../../src/config";
|
|
19
|
+
import type { PRD, UserStory } from "../../../src/prd/types";
|
|
20
|
+
import type { StoryMetrics } from "../../../src/metrics";
|
|
21
|
+
import type { VerificationResult } from "../../../src/verification";
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// Mock runVerification with call-order-based responses
|
|
@@ -41,54 +41,13 @@ const mockRevertStoriesOnFailure = mock(async ({ prd }: { prd: PRD; [k: string]:
|
|
|
41
41
|
const mockRunRectificationLoop = mock(async () => false);
|
|
42
42
|
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
|
-
//
|
|
44
|
+
// Static imports — uses _postVerifyDeps pattern (no mock.module() needed)
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
|
-
mock.module("../../src/execution/post-verify-rectification", () => ({
|
|
54
|
-
revertStoriesOnFailure: mockRevertStoriesOnFailure,
|
|
55
|
-
runRectificationLoop: mockRunRectificationLoop,
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
mock.module("../../src/prd", () => ({
|
|
59
|
-
getExpectedFiles: () => [],
|
|
60
|
-
savePRD: mock(async () => {}),
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
mock.module("../../src/execution/progress", () => ({
|
|
64
|
-
appendProgress: mock(async () => {}),
|
|
65
|
-
}));
|
|
66
|
-
|
|
67
|
-
mock.module("../../src/execution/escalation", () => ({
|
|
68
|
-
getTierConfig: () => undefined,
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
mock.module("../../src/verification/parser", () => ({
|
|
72
|
-
parseBunTestOutput: () => ({ failed: 0, passed: 5, failures: [] }),
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
mock.module("../../src/logger", () => ({
|
|
76
|
-
getSafeLogger: () => ({
|
|
77
|
-
info: () => {},
|
|
78
|
-
warn: () => {},
|
|
79
|
-
debug: () => {},
|
|
80
|
-
error: () => {},
|
|
81
|
-
}),
|
|
82
|
-
getLogger: () => ({
|
|
83
|
-
info: () => {},
|
|
84
|
-
warn: () => {},
|
|
85
|
-
debug: () => {},
|
|
86
|
-
error: () => {},
|
|
87
|
-
}),
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
// Dynamic import after mocks
|
|
91
|
-
const { runPostAgentVerification } = await import("../../src/execution/post-verify");
|
|
47
|
+
import { _postVerifyDeps, runPostAgentVerification } from "../../../src/execution/post-verify";
|
|
48
|
+
|
|
49
|
+
// ── Capture originals for afterEach restoration ───────────────────────────────
|
|
50
|
+
const _origPostVerifyDeps = { ..._postVerifyDeps };
|
|
92
51
|
|
|
93
52
|
// ---------------------------------------------------------------------------
|
|
94
53
|
// Fixtures
|
|
@@ -283,6 +242,17 @@ let tempDir: string;
|
|
|
283
242
|
let storyGitRef: string;
|
|
284
243
|
|
|
285
244
|
beforeEach(() => {
|
|
245
|
+
// Wire _postVerifyDeps to mocks
|
|
246
|
+
_postVerifyDeps.runVerification = mockRunVerification as typeof _postVerifyDeps.runVerification;
|
|
247
|
+
_postVerifyDeps.parseTestOutput = () => ({ passCount: 5, failCount: 0, isEnvironmentalFailure: false }) as any;
|
|
248
|
+
_postVerifyDeps.getEnvironmentalEscalationThreshold = () => 3;
|
|
249
|
+
_postVerifyDeps.revertStoriesOnFailure = mockRevertStoriesOnFailure as typeof _postVerifyDeps.revertStoriesOnFailure;
|
|
250
|
+
_postVerifyDeps.runRectificationLoop = mockRunRectificationLoop as typeof _postVerifyDeps.runRectificationLoop;
|
|
251
|
+
_postVerifyDeps.getExpectedFiles = () => [];
|
|
252
|
+
_postVerifyDeps.savePRD = mock(async () => {}) as typeof _postVerifyDeps.savePRD;
|
|
253
|
+
_postVerifyDeps.appendProgress = mock(async () => {}) as typeof _postVerifyDeps.appendProgress;
|
|
254
|
+
_postVerifyDeps.getTierConfig = () => undefined as any;
|
|
255
|
+
_postVerifyDeps.parseBunTestOutput = () => ({ failed: 0, passed: 5, failures: [] }) as any;
|
|
286
256
|
mockRunVerification.mockClear();
|
|
287
257
|
mockRevertStoriesOnFailure.mockClear();
|
|
288
258
|
mockRunRectificationLoop.mockClear();
|
|
@@ -295,6 +265,8 @@ beforeEach(() => {
|
|
|
295
265
|
});
|
|
296
266
|
|
|
297
267
|
afterEach(() => {
|
|
268
|
+
Object.assign(_postVerifyDeps, _origPostVerifyDeps);
|
|
269
|
+
mock.restore();
|
|
298
270
|
rmSync(tempDir, { recursive: true, force: true });
|
|
299
271
|
});
|
|
300
272
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, test } from "bun:test";
|
|
11
|
-
import type { RegressionGateConfig } from "
|
|
11
|
+
import type { RegressionGateConfig } from "../../../src/config/schema";
|
|
12
12
|
|
|
13
13
|
describe("RegressionGateConfig", () => {
|
|
14
14
|
test("should have correct default values", () => {
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* a fresh classification.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
10
|
import { initLogger, resetLogger } from "../../../src/logger";
|
|
11
|
-
import
|
|
11
|
+
import { _routingDeps, routingStage } from "../../../src/pipeline/stages/routing";
|
|
12
12
|
import type { NaxConfig } from "../../../src/config";
|
|
13
|
+
import type { PipelineContext } from "../../../src/pipeline/types";
|
|
13
14
|
import type { UserStory } from "../../../src/prd/types";
|
|
14
15
|
|
|
15
|
-
// ──
|
|
16
|
+
// ── Mock functions ────────────────────────────────────────────────────────────
|
|
16
17
|
|
|
17
18
|
const mockRouteStory = mock(async () => ({
|
|
18
19
|
complexity: "medium",
|
|
@@ -22,26 +23,11 @@ const mockRouteStory = mock(async () => ({
|
|
|
22
23
|
}));
|
|
23
24
|
|
|
24
25
|
const mockComplexityToModelTier = mock((_complexity: string, _config: unknown) => "balanced" as const);
|
|
26
|
+
const mockIsGreenfieldStory = mock(async () => false);
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
routeStory: mockRouteStory,
|
|
28
|
-
complexityToModelTier: mockComplexityToModelTier,
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
// Greenfield check: return false so it never interferes with test strategy
|
|
32
|
-
mock.module("../../../src/context/greenfield", () => ({
|
|
33
|
-
isGreenfieldStory: mock(async () => false),
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
// LLM batch cache is not relevant here
|
|
37
|
-
mock.module("../../../src/routing/strategies/llm", () => ({
|
|
38
|
-
clearCache: mock(() => {}),
|
|
39
|
-
routeBatch: mock(async () => []),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
// ── Dynamic imports after mocks ───────────────────────────────────────────────
|
|
28
|
+
// ── Capture originals for afterEach restoration ───────────────────────────────
|
|
43
29
|
|
|
44
|
-
const {
|
|
30
|
+
const _origDeps = { ..._routingDeps };
|
|
45
31
|
|
|
46
32
|
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
47
33
|
|
|
@@ -58,11 +44,9 @@ function makeStory(routingOverride?: Partial<UserStory["routing"]>): UserStory {
|
|
|
58
44
|
tags: [],
|
|
59
45
|
dependencies: [],
|
|
60
46
|
};
|
|
61
|
-
|
|
62
47
|
if (routingOverride !== undefined) {
|
|
63
48
|
story.routing = routingOverride as UserStory["routing"];
|
|
64
49
|
}
|
|
65
|
-
|
|
66
50
|
return story;
|
|
67
51
|
}
|
|
68
52
|
|
|
@@ -82,16 +66,22 @@ function makeCtx(story: UserStory): PipelineContext {
|
|
|
82
66
|
} as PipelineContext;
|
|
83
67
|
}
|
|
84
68
|
|
|
85
|
-
// ──
|
|
69
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────────
|
|
86
70
|
|
|
87
71
|
beforeEach(() => {
|
|
88
72
|
resetLogger();
|
|
89
73
|
initLogger({ level: "error", useChalk: false });
|
|
74
|
+
_routingDeps.routeStory = mockRouteStory as typeof _routingDeps.routeStory;
|
|
75
|
+
_routingDeps.complexityToModelTier = mockComplexityToModelTier as typeof _routingDeps.complexityToModelTier;
|
|
76
|
+
_routingDeps.isGreenfieldStory = mockIsGreenfieldStory as typeof _routingDeps.isGreenfieldStory;
|
|
90
77
|
mockRouteStory.mockClear();
|
|
91
78
|
mockComplexityToModelTier.mockClear();
|
|
79
|
+
mockIsGreenfieldStory.mockClear();
|
|
92
80
|
});
|
|
93
81
|
|
|
94
82
|
afterEach(() => {
|
|
83
|
+
Object.assign(_routingDeps, _origDeps);
|
|
84
|
+
mock.restore();
|
|
95
85
|
resetLogger();
|
|
96
86
|
});
|
|
97
87
|
|
|
@@ -99,42 +89,31 @@ afterEach(() => {
|
|
|
99
89
|
|
|
100
90
|
describe("routing stage — partial override (FIX-001)", () => {
|
|
101
91
|
test("(1) partial override with only testStrategy preserves LLM complexity", async () => {
|
|
102
|
-
// Story sets only testStrategy — complexity should come from LLM
|
|
103
92
|
const story = makeStory({ testStrategy: "test-after", complexity: undefined as any, reasoning: "manual" });
|
|
104
93
|
const ctx = makeCtx(story);
|
|
105
94
|
|
|
106
95
|
await routingStage.execute(ctx);
|
|
107
96
|
|
|
108
|
-
// testStrategy is overridden by the story field
|
|
109
97
|
expect(ctx.routing.testStrategy).toBe("test-after");
|
|
110
|
-
// complexity should remain from the LLM result ("medium"), not undefined
|
|
111
98
|
expect(ctx.routing.complexity).toBe("medium");
|
|
112
99
|
});
|
|
113
100
|
|
|
114
101
|
test("(2) LLM-classified complexity is preserved when story.routing has no complexity", async () => {
|
|
115
|
-
// story.routing is present but complexity is undefined (falsy)
|
|
116
102
|
const story = makeStory({ testStrategy: "test-after", complexity: undefined as any, reasoning: "" });
|
|
117
103
|
const ctx = makeCtx(story);
|
|
118
104
|
|
|
119
105
|
await routingStage.execute(ctx);
|
|
120
106
|
|
|
121
|
-
// LLM returned "medium" — it must not be overwritten with undefined
|
|
122
107
|
expect(ctx.routing.complexity).toBe("medium");
|
|
123
108
|
expect(ctx.routing.complexity).not.toBeUndefined();
|
|
124
109
|
});
|
|
125
110
|
|
|
126
111
|
test("(3) full override works when both complexity and testStrategy are set", async () => {
|
|
127
|
-
|
|
128
|
-
const story = makeStory({
|
|
129
|
-
complexity: "simple",
|
|
130
|
-
testStrategy: "test-after",
|
|
131
|
-
reasoning: "manual override",
|
|
132
|
-
});
|
|
112
|
+
const story = makeStory({ complexity: "simple", testStrategy: "test-after", reasoning: "manual override" });
|
|
133
113
|
const ctx = makeCtx(story);
|
|
134
114
|
|
|
135
115
|
await routingStage.execute(ctx);
|
|
136
116
|
|
|
137
|
-
// Both fields should be overridden from the story
|
|
138
117
|
expect(ctx.routing.complexity).toBe("simple");
|
|
139
118
|
expect(ctx.routing.testStrategy).toBe("test-after");
|
|
140
119
|
});
|
|
@@ -23,15 +23,12 @@ import type { PRD, UserStory } from "../../../src/prd/types";
|
|
|
23
23
|
|
|
24
24
|
const mockRegression = mock(async () => ({ success: true, status: "SUCCESS" as const }));
|
|
25
25
|
|
|
26
|
-
mock.module(
|
|
27
|
-
|
|
28
|
-
}));
|
|
26
|
+
// ---- Static imports — no mock.module() needed (uses _deps pattern) ----------
|
|
27
|
+
import { _verifyDeps, verifyStage } from "../../../src/pipeline/stages/verify";
|
|
29
28
|
|
|
30
29
|
// ---- Capture originals for afterEach restoration ----------------------------
|
|
31
30
|
const _origDeps = { ..._smartRunnerDeps };
|
|
32
|
-
|
|
33
|
-
// ---- Dynamic import after gate mock -----------------------------------------
|
|
34
|
-
const { verifyStage } = await import("../../../src/pipeline/stages/verify");
|
|
31
|
+
const _origVerifyDeps = { ..._verifyDeps };
|
|
35
32
|
|
|
36
33
|
// ---- Mock functions ---------------------------------------------------------
|
|
37
34
|
|
|
@@ -160,6 +157,7 @@ describe("Verify Stage --- Smart Runner Integration", () => {
|
|
|
160
157
|
_smartRunnerDeps.mapSourceToTests = mockMapSourceToTests;
|
|
161
158
|
_smartRunnerDeps.importGrepFallback = mockImportGrepFallback;
|
|
162
159
|
_smartRunnerDeps.buildSmartTestCommand = mockBuildSmartTestCommand;
|
|
160
|
+
_verifyDeps.regression = mockRegression as typeof _verifyDeps.regression;
|
|
163
161
|
mockRegression.mockClear();
|
|
164
162
|
mockGetChangedSourceFiles.mockClear();
|
|
165
163
|
mockMapSourceToTests.mockClear();
|
|
@@ -170,6 +168,7 @@ describe("Verify Stage --- Smart Runner Integration", () => {
|
|
|
170
168
|
afterEach(() => {
|
|
171
169
|
resetLogger();
|
|
172
170
|
Object.assign(_smartRunnerDeps, _origDeps);
|
|
171
|
+
Object.assign(_verifyDeps, _origVerifyDeps);
|
|
173
172
|
});
|
|
174
173
|
|
|
175
174
|
describe("AC1: uses scoped test command when smart runner finds test files", () => {
|