@nathapp/nax 0.18.1 → 0.18.2
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/.gitlab-ci.yml +2 -2
- package/docs/ROADMAP.md +21 -23
- package/nax/config.json +12 -9
- package/nax/features/smart-test-runner/plan.md +7 -0
- package/nax/features/smart-test-runner/prd.json +203 -0
- package/nax/features/smart-test-runner/progress.txt +13 -0
- package/nax/features/smart-test-runner/spec.md +7 -0
- package/nax/features/smart-test-runner/tasks.md +8 -0
- package/package.json +1 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schemas.ts +1 -0
- package/src/config/types.ts +2 -0
- package/src/pipeline/stages/routing.ts +4 -4
- package/src/pipeline/stages/verify.ts +20 -1
- package/src/precheck/index.ts +9 -4
- package/src/verification/smart-runner.ts +146 -0
- package/test/US-002-orchestrator.test.ts +5 -5
- package/test/unit/config/smart-runner-flag.test.ts +225 -0
- package/test/unit/pipeline/routing-partial-override.test.ts +141 -0
- package/test/unit/pipeline/verify-smart-runner.test.ts +341 -0
- package/test/unit/verification/smart-runner.test.ts +246 -0
|
@@ -101,7 +101,7 @@ describe("US-002: Precheck orchestrator acceptance criteria", () => {
|
|
|
101
101
|
const config = createConfig(testDir);
|
|
102
102
|
const prd = createPRD();
|
|
103
103
|
|
|
104
|
-
const { result } = await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
104
|
+
const { result } = await runPrecheck(config, prd, { workdir: testDir, format: "json", silent: true });
|
|
105
105
|
|
|
106
106
|
// Should have exactly 1 blocker (fail-fast)
|
|
107
107
|
expect(result.blockers.length).toBe(1);
|
|
@@ -116,7 +116,7 @@ describe("US-002: Precheck orchestrator acceptance criteria", () => {
|
|
|
116
116
|
const config = createConfig(testDir);
|
|
117
117
|
const prd = createPRD();
|
|
118
118
|
|
|
119
|
-
const { result } = await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
119
|
+
const { result } = await runPrecheck(config, prd, { workdir: testDir, format: "json", silent: true });
|
|
120
120
|
|
|
121
121
|
// No blockers
|
|
122
122
|
expect(result.blockers.length).toBe(0);
|
|
@@ -195,13 +195,13 @@ describe("US-002: Precheck orchestrator acceptance criteria", () => {
|
|
|
195
195
|
|
|
196
196
|
let config = createConfig(testDir);
|
|
197
197
|
const prd = createPRD();
|
|
198
|
-
let result = await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
198
|
+
let result = await runPrecheck(config, prd, { workdir: testDir, format: "json", silent: true });
|
|
199
199
|
expect(result.exitCode).toBe(EXIT_CODES.SUCCESS);
|
|
200
200
|
|
|
201
201
|
// Test exit code 1 (blocker)
|
|
202
202
|
const testDir2 = mkdtempSync(join(tmpdir(), "nax-test-blocker-"));
|
|
203
203
|
config = createConfig(testDir2);
|
|
204
|
-
result = await runPrecheck(config, prd, { workdir: testDir2, format: "json" });
|
|
204
|
+
result = await runPrecheck(config, prd, { workdir: testDir2, format: "json", silent: true });
|
|
205
205
|
expect(result.exitCode).toBe(EXIT_CODES.BLOCKER);
|
|
206
206
|
rmSync(testDir2, { recursive: true, force: true });
|
|
207
207
|
|
|
@@ -214,7 +214,7 @@ describe("US-002: Precheck orchestrator acceptance criteria", () => {
|
|
|
214
214
|
...prd,
|
|
215
215
|
userStories: [{ ...prd.userStories[0], id: "", title: "", description: "" }],
|
|
216
216
|
};
|
|
217
|
-
result = await runPrecheck(config, invalidPRD, { workdir: testDir3, format: "json" });
|
|
217
|
+
result = await runPrecheck(config, invalidPRD, { workdir: testDir3, format: "json", silent: true });
|
|
218
218
|
expect(result.exitCode).toBe(EXIT_CODES.INVALID_PRD);
|
|
219
219
|
rmSync(testDir3, { recursive: true, force: true });
|
|
220
220
|
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Test Runner Config Flag Tests (STR-004)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that execution.smartTestRunner is present in the ExecutionConfig
|
|
5
|
+
* interface, defaults to true in the Zod schema, and loads correctly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { globalConfigPath, loadConfig } from "../../../src/config/loader";
|
|
13
|
+
import { DEFAULT_CONFIG } from "../../../src/config/defaults";
|
|
14
|
+
import { NaxConfigSchema } from "../../../src/config/schemas";
|
|
15
|
+
|
|
16
|
+
describe("execution.smartTestRunner config flag", () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
let globalBackup: string | null = null;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tempDir = join(tmpdir(), `nax-str-004-${Date.now()}`);
|
|
22
|
+
mkdirSync(join(tempDir, "nax"), { recursive: true });
|
|
23
|
+
|
|
24
|
+
const globalPath = globalConfigPath();
|
|
25
|
+
if (existsSync(globalPath)) {
|
|
26
|
+
globalBackup = `${globalPath}.test-backup-${Date.now()}`;
|
|
27
|
+
renameSync(globalPath, globalBackup);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (tempDir) {
|
|
33
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
if (globalBackup && existsSync(globalBackup)) {
|
|
36
|
+
const globalPath = globalConfigPath();
|
|
37
|
+
if (existsSync(globalPath)) rmSync(globalPath);
|
|
38
|
+
renameSync(globalBackup, globalPath);
|
|
39
|
+
globalBackup = null;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("DEFAULT_CONFIG has smartTestRunner: true", () => {
|
|
44
|
+
expect(DEFAULT_CONFIG.execution.smartTestRunner).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("Zod schema defaults smartTestRunner to true when field is absent", () => {
|
|
48
|
+
const minimal = {
|
|
49
|
+
version: 1,
|
|
50
|
+
models: {
|
|
51
|
+
fast: { provider: "anthropic", model: "haiku" },
|
|
52
|
+
balanced: { provider: "anthropic", model: "sonnet" },
|
|
53
|
+
powerful: { provider: "anthropic", model: "opus" },
|
|
54
|
+
},
|
|
55
|
+
autoMode: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
defaultAgent: "claude",
|
|
58
|
+
fallbackOrder: [],
|
|
59
|
+
complexityRouting: { simple: "fast", medium: "balanced", complex: "powerful", expert: "powerful" },
|
|
60
|
+
escalation: { enabled: true, tierOrder: [{ tier: "fast", attempts: 1 }] },
|
|
61
|
+
},
|
|
62
|
+
routing: { strategy: "keyword" },
|
|
63
|
+
execution: {
|
|
64
|
+
maxIterations: 10,
|
|
65
|
+
iterationDelayMs: 0,
|
|
66
|
+
costLimit: 1,
|
|
67
|
+
sessionTimeoutSeconds: 60,
|
|
68
|
+
maxStoriesPerFeature: 10,
|
|
69
|
+
rectification: {
|
|
70
|
+
enabled: true,
|
|
71
|
+
maxRetries: 1,
|
|
72
|
+
fullSuiteTimeoutSeconds: 30,
|
|
73
|
+
maxFailureSummaryChars: 500,
|
|
74
|
+
abortOnIncreasingFailures: true,
|
|
75
|
+
},
|
|
76
|
+
regressionGate: { enabled: true, timeoutSeconds: 30 },
|
|
77
|
+
contextProviderTokenBudget: 100,
|
|
78
|
+
// smartTestRunner intentionally omitted
|
|
79
|
+
},
|
|
80
|
+
quality: {
|
|
81
|
+
requireTypecheck: true,
|
|
82
|
+
requireLint: true,
|
|
83
|
+
requireTests: true,
|
|
84
|
+
commands: {},
|
|
85
|
+
forceExit: false,
|
|
86
|
+
detectOpenHandles: true,
|
|
87
|
+
detectOpenHandlesRetries: 1,
|
|
88
|
+
gracePeriodMs: 500,
|
|
89
|
+
drainTimeoutMs: 0,
|
|
90
|
+
shell: "/bin/sh",
|
|
91
|
+
stripEnvVars: [],
|
|
92
|
+
environmentalEscalationDivisor: 2,
|
|
93
|
+
},
|
|
94
|
+
tdd: { maxRetries: 0, autoVerifyIsolation: false, autoApproveVerifier: false },
|
|
95
|
+
constitution: { enabled: false, path: "constitution.md", maxTokens: 100 },
|
|
96
|
+
analyze: { llmEnhanced: false, model: "balanced", fallbackToKeywords: true, maxCodebaseSummaryTokens: 100 },
|
|
97
|
+
review: { enabled: false, checks: [], commands: {} },
|
|
98
|
+
plan: { model: "balanced", outputPath: "spec.md" },
|
|
99
|
+
acceptance: { enabled: false, maxRetries: 0, generateTests: false, testPath: "acceptance.test.ts" },
|
|
100
|
+
context: {
|
|
101
|
+
testCoverage: { enabled: false, detail: "names-only", maxTokens: 50, testPattern: "**/*.test.ts", scopeToStory: false },
|
|
102
|
+
autoDetect: { enabled: false, maxFiles: 1, traceImports: false },
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = NaxConfigSchema.safeParse(minimal);
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
if (result.success) {
|
|
109
|
+
expect(result.data.execution.smartTestRunner).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("Zod schema accepts smartTestRunner: false", () => {
|
|
114
|
+
const result = NaxConfigSchema.safeParse({
|
|
115
|
+
...buildMinimalConfig(),
|
|
116
|
+
execution: { ...buildMinimalConfig().execution, smartTestRunner: false },
|
|
117
|
+
});
|
|
118
|
+
expect(result.success).toBe(true);
|
|
119
|
+
if (result.success) {
|
|
120
|
+
expect(result.data.execution.smartTestRunner).toBe(false);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("Zod schema accepts smartTestRunner: true explicitly", () => {
|
|
125
|
+
const result = NaxConfigSchema.safeParse({
|
|
126
|
+
...buildMinimalConfig(),
|
|
127
|
+
execution: { ...buildMinimalConfig().execution, smartTestRunner: true },
|
|
128
|
+
});
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
if (result.success) {
|
|
131
|
+
expect(result.data.execution.smartTestRunner).toBe(true);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("loadConfig defaults smartTestRunner to true when not in project config", async () => {
|
|
136
|
+
const configPath = join(tempDir, "nax", "config.json");
|
|
137
|
+
writeFileSync(configPath, JSON.stringify({ routing: { strategy: "keyword" } }, null, 2));
|
|
138
|
+
|
|
139
|
+
const config = await loadConfig(join(tempDir, "nax"));
|
|
140
|
+
expect(config.execution.smartTestRunner).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("loadConfig respects smartTestRunner: false from project config", async () => {
|
|
144
|
+
const configPath = join(tempDir, "nax", "config.json");
|
|
145
|
+
writeFileSync(
|
|
146
|
+
configPath,
|
|
147
|
+
JSON.stringify({ execution: { smartTestRunner: false } }, null, 2),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const config = await loadConfig(join(tempDir, "nax"));
|
|
151
|
+
expect(config.execution.smartTestRunner).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("loadConfig loads correctly without the field (backward compat)", async () => {
|
|
155
|
+
const configPath = join(tempDir, "nax", "config.json");
|
|
156
|
+
// Config without smartTestRunner field at all
|
|
157
|
+
writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
158
|
+
|
|
159
|
+
const config = await loadConfig(join(tempDir, "nax"));
|
|
160
|
+
expect(config.execution.smartTestRunner).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Minimal valid config helper
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
function buildMinimalConfig() {
|
|
169
|
+
return {
|
|
170
|
+
version: 1,
|
|
171
|
+
models: {
|
|
172
|
+
fast: { provider: "anthropic", model: "haiku" },
|
|
173
|
+
balanced: { provider: "anthropic", model: "sonnet" },
|
|
174
|
+
powerful: { provider: "anthropic", model: "opus" },
|
|
175
|
+
},
|
|
176
|
+
autoMode: {
|
|
177
|
+
enabled: true,
|
|
178
|
+
defaultAgent: "claude",
|
|
179
|
+
fallbackOrder: [],
|
|
180
|
+
complexityRouting: { simple: "fast", medium: "balanced", complex: "powerful", expert: "powerful" },
|
|
181
|
+
escalation: { enabled: true, tierOrder: [{ tier: "fast", attempts: 1 }] },
|
|
182
|
+
},
|
|
183
|
+
routing: { strategy: "keyword" as const },
|
|
184
|
+
execution: {
|
|
185
|
+
maxIterations: 10,
|
|
186
|
+
iterationDelayMs: 0,
|
|
187
|
+
costLimit: 1,
|
|
188
|
+
sessionTimeoutSeconds: 60,
|
|
189
|
+
maxStoriesPerFeature: 10,
|
|
190
|
+
rectification: {
|
|
191
|
+
enabled: true,
|
|
192
|
+
maxRetries: 1,
|
|
193
|
+
fullSuiteTimeoutSeconds: 30,
|
|
194
|
+
maxFailureSummaryChars: 500,
|
|
195
|
+
abortOnIncreasingFailures: true,
|
|
196
|
+
},
|
|
197
|
+
regressionGate: { enabled: true, timeoutSeconds: 30 },
|
|
198
|
+
contextProviderTokenBudget: 100,
|
|
199
|
+
},
|
|
200
|
+
quality: {
|
|
201
|
+
requireTypecheck: true,
|
|
202
|
+
requireLint: true,
|
|
203
|
+
requireTests: true,
|
|
204
|
+
commands: {},
|
|
205
|
+
forceExit: false,
|
|
206
|
+
detectOpenHandles: true,
|
|
207
|
+
detectOpenHandlesRetries: 1,
|
|
208
|
+
gracePeriodMs: 500,
|
|
209
|
+
drainTimeoutMs: 0,
|
|
210
|
+
shell: "/bin/sh",
|
|
211
|
+
stripEnvVars: [],
|
|
212
|
+
environmentalEscalationDivisor: 2,
|
|
213
|
+
},
|
|
214
|
+
tdd: { maxRetries: 0, autoVerifyIsolation: false, autoApproveVerifier: false },
|
|
215
|
+
constitution: { enabled: false, path: "constitution.md", maxTokens: 100 },
|
|
216
|
+
analyze: { llmEnhanced: false, model: "balanced", fallbackToKeywords: true, maxCodebaseSummaryTokens: 100 },
|
|
217
|
+
review: { enabled: false, checks: [] as Array<"typecheck" | "lint" | "test">, commands: {} },
|
|
218
|
+
plan: { model: "balanced", outputPath: "spec.md" },
|
|
219
|
+
acceptance: { enabled: false, maxRetries: 0, generateTests: false, testPath: "acceptance.test.ts" },
|
|
220
|
+
context: {
|
|
221
|
+
testCoverage: { enabled: false, detail: "names-only" as const, maxTokens: 50, testPattern: "**/*.test.ts", scopeToStory: false },
|
|
222
|
+
autoDetect: { enabled: false, maxFiles: 1, traceImports: false },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for partial routing override in routing stage (FIX-001)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that story.routing fields only override LLM-classified results
|
|
5
|
+
* when they are actually set. Prevents undefined values from clobbering
|
|
6
|
+
* a fresh classification.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, afterEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
import { initLogger, resetLogger } from "../../../src/logger";
|
|
11
|
+
import type { PipelineContext } from "../../../src/pipeline/types";
|
|
12
|
+
import type { NaxConfig } from "../../../src/config";
|
|
13
|
+
import type { UserStory } from "../../../src/prd/types";
|
|
14
|
+
|
|
15
|
+
// ── Module mocks (must be declared before dynamic imports) ────────────────────
|
|
16
|
+
|
|
17
|
+
const mockRouteStory = mock(async () => ({
|
|
18
|
+
complexity: "medium",
|
|
19
|
+
modelTier: "balanced",
|
|
20
|
+
testStrategy: "three-session-tdd",
|
|
21
|
+
reasoning: "LLM classified as medium",
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const mockComplexityToModelTier = mock((_complexity: string, _config: unknown) => "balanced" as const);
|
|
25
|
+
|
|
26
|
+
mock.module("../../../src/routing", () => ({
|
|
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 ───────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const { routingStage } = await import("../../../src/pipeline/stages/routing");
|
|
45
|
+
|
|
46
|
+
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function makeStory(routingOverride?: Partial<UserStory["routing"]>): UserStory {
|
|
49
|
+
const story: UserStory = {
|
|
50
|
+
id: "FIX-001-test",
|
|
51
|
+
title: "Partial routing override test",
|
|
52
|
+
description: "Tests that story.routing only overrides when set",
|
|
53
|
+
acceptanceCriteria: [],
|
|
54
|
+
status: "pending",
|
|
55
|
+
passes: false,
|
|
56
|
+
escalations: [],
|
|
57
|
+
attempts: 0,
|
|
58
|
+
tags: [],
|
|
59
|
+
dependencies: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (routingOverride !== undefined) {
|
|
63
|
+
story.routing = routingOverride as UserStory["routing"];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return story;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeCtx(story: UserStory): PipelineContext {
|
|
70
|
+
return {
|
|
71
|
+
config: {
|
|
72
|
+
tdd: { greenfieldDetection: false },
|
|
73
|
+
autoMode: { complexityRouting: {} },
|
|
74
|
+
routing: { strategy: "complexity", llm: { mode: "per-story" } },
|
|
75
|
+
} as unknown as NaxConfig,
|
|
76
|
+
story,
|
|
77
|
+
stories: [story],
|
|
78
|
+
routing: {} as PipelineContext["routing"],
|
|
79
|
+
workdir: "/tmp/nax-test-partial-routing",
|
|
80
|
+
prd: { feature: "test", userStories: [story] } as PipelineContext["prd"],
|
|
81
|
+
hooks: {} as PipelineContext["hooks"],
|
|
82
|
+
} as PipelineContext;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Logger setup ──────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
resetLogger();
|
|
89
|
+
initLogger({ level: "error", useChalk: false });
|
|
90
|
+
mockRouteStory.mockClear();
|
|
91
|
+
mockComplexityToModelTier.mockClear();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
resetLogger();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("routing stage — partial override (FIX-001)", () => {
|
|
101
|
+
test("(1) partial override with only testStrategy preserves LLM complexity", async () => {
|
|
102
|
+
// Story sets only testStrategy — complexity should come from LLM
|
|
103
|
+
const story = makeStory({ testStrategy: "test-after", complexity: undefined as any, reasoning: "manual" });
|
|
104
|
+
const ctx = makeCtx(story);
|
|
105
|
+
|
|
106
|
+
await routingStage.execute(ctx);
|
|
107
|
+
|
|
108
|
+
// testStrategy is overridden by the story field
|
|
109
|
+
expect(ctx.routing.testStrategy).toBe("test-after");
|
|
110
|
+
// complexity should remain from the LLM result ("medium"), not undefined
|
|
111
|
+
expect(ctx.routing.complexity).toBe("medium");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
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
|
+
const story = makeStory({ testStrategy: "test-after", complexity: undefined as any, reasoning: "" });
|
|
117
|
+
const ctx = makeCtx(story);
|
|
118
|
+
|
|
119
|
+
await routingStage.execute(ctx);
|
|
120
|
+
|
|
121
|
+
// LLM returned "medium" — it must not be overwritten with undefined
|
|
122
|
+
expect(ctx.routing.complexity).toBe("medium");
|
|
123
|
+
expect(ctx.routing.complexity).not.toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("(3) full override works when both complexity and testStrategy are set", async () => {
|
|
127
|
+
// Story has explicit values for both fields
|
|
128
|
+
const story = makeStory({
|
|
129
|
+
complexity: "simple",
|
|
130
|
+
testStrategy: "test-after",
|
|
131
|
+
reasoning: "manual override",
|
|
132
|
+
});
|
|
133
|
+
const ctx = makeCtx(story);
|
|
134
|
+
|
|
135
|
+
await routingStage.execute(ctx);
|
|
136
|
+
|
|
137
|
+
// Both fields should be overridden from the story
|
|
138
|
+
expect(ctx.routing.complexity).toBe("simple");
|
|
139
|
+
expect(ctx.routing.testStrategy).toBe("test-after");
|
|
140
|
+
});
|
|
141
|
+
});
|