@nathapp/nax 0.40.0 → 0.40.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/dist/nax.js +97 -12
- package/package.json +1 -1
- package/src/acceptance/generator.ts +23 -1
- package/src/acceptance/refinement.ts +72 -5
- package/src/acceptance/templates/cli.ts +47 -0
- package/src/acceptance/templates/component.ts +78 -0
- package/src/acceptance/templates/e2e.ts +43 -0
- package/src/acceptance/templates/index.ts +21 -0
- package/src/acceptance/templates/snapshot.ts +50 -0
- package/src/acceptance/templates/unit.ts +48 -0
- package/src/acceptance/types.ts +9 -1
- package/src/cli/init-detect.ts +94 -8
- package/src/cli/init.ts +2 -2
- package/src/config/index.ts +1 -1
- package/src/config/runtime-types.ts +7 -0
- package/src/config/schema.ts +2 -1
- package/src/config/schemas.ts +3 -1
- package/src/config/types.ts +1 -0
- package/src/pipeline/stages/acceptance-setup.ts +4 -0
package/dist/nax.js
CHANGED
|
@@ -17708,7 +17708,9 @@ var init_schemas3 = __esm(() => {
|
|
|
17708
17708
|
testPath: exports_external.string().min(1, "acceptance.testPath must be non-empty"),
|
|
17709
17709
|
model: exports_external.enum(["fast", "balanced", "powerful"]).default("fast"),
|
|
17710
17710
|
refinement: exports_external.boolean().default(true),
|
|
17711
|
-
redGate: exports_external.boolean().default(true)
|
|
17711
|
+
redGate: exports_external.boolean().default(true),
|
|
17712
|
+
testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
|
|
17713
|
+
testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional()
|
|
17712
17714
|
});
|
|
17713
17715
|
TestCoverageConfigSchema = exports_external.object({
|
|
17714
17716
|
enabled: exports_external.boolean().default(true),
|
|
@@ -18362,14 +18364,16 @@ __export(exports_refinement, {
|
|
|
18362
18364
|
buildRefinementPrompt: () => buildRefinementPrompt,
|
|
18363
18365
|
_refineDeps: () => _refineDeps
|
|
18364
18366
|
});
|
|
18365
|
-
function buildRefinementPrompt(criteria, codebaseContext) {
|
|
18367
|
+
function buildRefinementPrompt(criteria, codebaseContext, options) {
|
|
18366
18368
|
const criteriaList = criteria.map((c, i) => `${i + 1}. ${c}`).join(`
|
|
18367
18369
|
`);
|
|
18370
|
+
const strategySection = buildStrategySection(options);
|
|
18371
|
+
const refinedExample = buildRefinedExample(options?.testStrategy);
|
|
18368
18372
|
return `You are an acceptance criteria refinement assistant. Your task is to convert raw acceptance criteria into concrete, machine-verifiable assertions.
|
|
18369
18373
|
|
|
18370
18374
|
CODEBASE CONTEXT:
|
|
18371
18375
|
${codebaseContext}
|
|
18372
|
-
|
|
18376
|
+
${strategySection}
|
|
18373
18377
|
ACCEPTANCE CRITERIA TO REFINE:
|
|
18374
18378
|
${criteriaList}
|
|
18375
18379
|
|
|
@@ -18384,11 +18388,53 @@ Respond with ONLY a JSON array (no markdown code fences):
|
|
|
18384
18388
|
|
|
18385
18389
|
Rules:
|
|
18386
18390
|
- "original" must match the input criterion text exactly
|
|
18387
|
-
- "refined" must be a concrete assertion (e.g.,
|
|
18391
|
+
- "refined" must be a concrete assertion (e.g., ${refinedExample})
|
|
18388
18392
|
- "testable" is false only if the criterion cannot be automatically verified (e.g., "UX feels responsive", "design looks good")
|
|
18389
18393
|
- "storyId" leave as empty string \u2014 it will be assigned by the caller
|
|
18390
18394
|
- Respond with ONLY the JSON array`;
|
|
18391
18395
|
}
|
|
18396
|
+
function buildStrategySection(options) {
|
|
18397
|
+
if (!options?.testStrategy) {
|
|
18398
|
+
return "";
|
|
18399
|
+
}
|
|
18400
|
+
const framework = options.testFramework ? ` Use ${options.testFramework} testing library syntax.` : "";
|
|
18401
|
+
switch (options.testStrategy) {
|
|
18402
|
+
case "component":
|
|
18403
|
+
return `
|
|
18404
|
+
TEST STRATEGY: component
|
|
18405
|
+
Focus assertions on rendered output visible on screen \u2014 text content, visible elements, and screen state.
|
|
18406
|
+
Assert what the user sees rendered in the component, not what internal functions produce.${framework}
|
|
18407
|
+
`;
|
|
18408
|
+
case "cli":
|
|
18409
|
+
return `
|
|
18410
|
+
TEST STRATEGY: cli
|
|
18411
|
+
Focus assertions on stdout and stderr text output from the CLI command.
|
|
18412
|
+
Assert about terminal output content, exit codes, and standard output/standard error streams.${framework}
|
|
18413
|
+
`;
|
|
18414
|
+
case "e2e":
|
|
18415
|
+
return `
|
|
18416
|
+
TEST STRATEGY: e2e
|
|
18417
|
+
Focus assertions on HTTP response content \u2014 status codes, response bodies, and endpoint behavior.
|
|
18418
|
+
Assert about HTTP responses, status codes, and API endpoint output.${framework}
|
|
18419
|
+
`;
|
|
18420
|
+
default:
|
|
18421
|
+
return framework ? `
|
|
18422
|
+
TEST FRAMEWORK: ${options.testFramework}
|
|
18423
|
+
` : "";
|
|
18424
|
+
}
|
|
18425
|
+
}
|
|
18426
|
+
function buildRefinedExample(testStrategy) {
|
|
18427
|
+
switch (testStrategy) {
|
|
18428
|
+
case "component":
|
|
18429
|
+
return '"Text content visible on screen matches expected", "Rendered output contains expected element"';
|
|
18430
|
+
case "cli":
|
|
18431
|
+
return '"stdout contains expected text", "stderr is empty on success", "exit code is 0"';
|
|
18432
|
+
case "e2e":
|
|
18433
|
+
return '"HTTP status 200 returned", "Response body contains expected field", "Endpoint returns JSON"';
|
|
18434
|
+
default:
|
|
18435
|
+
return '"Array of length N returned", "HTTP status 200 returned"';
|
|
18436
|
+
}
|
|
18437
|
+
}
|
|
18392
18438
|
function parseRefinementResponse(response, criteria) {
|
|
18393
18439
|
if (!response || !response.trim()) {
|
|
18394
18440
|
return fallbackCriteria(criteria);
|
|
@@ -18412,7 +18458,7 @@ async function refineAcceptanceCriteria(criteria, context) {
|
|
|
18412
18458
|
if (criteria.length === 0) {
|
|
18413
18459
|
return [];
|
|
18414
18460
|
}
|
|
18415
|
-
const { storyId, codebaseContext, config: config2 } = context;
|
|
18461
|
+
const { storyId, codebaseContext, config: config2, testStrategy, testFramework } = context;
|
|
18416
18462
|
const logger = getLogger();
|
|
18417
18463
|
const modelTier = config2.acceptance?.model ?? "fast";
|
|
18418
18464
|
const modelEntry = config2.models[modelTier] ?? config2.models.fast;
|
|
@@ -18420,7 +18466,7 @@ async function refineAcceptanceCriteria(criteria, context) {
|
|
|
18420
18466
|
throw new Error(`[refinement] config.models.${modelTier} not configured`);
|
|
18421
18467
|
}
|
|
18422
18468
|
const modelDef = resolveModel(modelEntry);
|
|
18423
|
-
const prompt = buildRefinementPrompt(criteria, codebaseContext);
|
|
18469
|
+
const prompt = buildRefinementPrompt(criteria, codebaseContext, { testStrategy, testFramework });
|
|
18424
18470
|
let response;
|
|
18425
18471
|
try {
|
|
18426
18472
|
response = await _refineDeps.adapter.complete(prompt, {
|
|
@@ -18483,6 +18529,7 @@ async function generateFromPRD(_stories, refinedCriteria, options) {
|
|
|
18483
18529
|
}
|
|
18484
18530
|
const criteriaList = refinedCriteria.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
|
|
18485
18531
|
`);
|
|
18532
|
+
const strategyInstructions = buildStrategyInstructions(options.testStrategy, options.testFramework);
|
|
18486
18533
|
const prompt = `You are a test engineer. Generate acceptance tests for the "${options.featureName}" feature based on the refined acceptance criteria below.
|
|
18487
18534
|
|
|
18488
18535
|
CODEBASE CONTEXT:
|
|
@@ -18491,7 +18538,7 @@ ${options.codebaseContext}
|
|
|
18491
18538
|
ACCEPTANCE CRITERIA (refined):
|
|
18492
18539
|
${criteriaList}
|
|
18493
18540
|
|
|
18494
|
-
Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
|
|
18541
|
+
${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
|
|
18495
18542
|
|
|
18496
18543
|
Use this structure:
|
|
18497
18544
|
|
|
@@ -18518,6 +18565,40 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
|
|
|
18518
18565
|
await _generatorPRDDeps.writeFile(join2(options.workdir, "acceptance-refined.json"), refinedJsonContent);
|
|
18519
18566
|
return { testCode, criteria };
|
|
18520
18567
|
}
|
|
18568
|
+
function buildStrategyInstructions(strategy, framework) {
|
|
18569
|
+
switch (strategy) {
|
|
18570
|
+
case "component": {
|
|
18571
|
+
const fw = framework ?? "ink-testing-library";
|
|
18572
|
+
if (fw === "react") {
|
|
18573
|
+
return `TEST STRATEGY: component (react)
|
|
18574
|
+
Import render and screen from @testing-library/react. Render the component and use screen.getByText to assert on output.
|
|
18575
|
+
|
|
18576
|
+
`;
|
|
18577
|
+
}
|
|
18578
|
+
return `TEST STRATEGY: component (ink-testing-library)
|
|
18579
|
+
Import render from ink-testing-library. Render the component and use lastFrame() to assert on output.
|
|
18580
|
+
|
|
18581
|
+
`;
|
|
18582
|
+
}
|
|
18583
|
+
case "cli":
|
|
18584
|
+
return `TEST STRATEGY: cli
|
|
18585
|
+
Use Bun.spawn to run the binary. Read stdout and assert on the text output.
|
|
18586
|
+
|
|
18587
|
+
`;
|
|
18588
|
+
case "e2e":
|
|
18589
|
+
return `TEST STRATEGY: e2e
|
|
18590
|
+
Use fetch() against http://localhost to call the running service. Assert on response body using response.text() or response.json().
|
|
18591
|
+
|
|
18592
|
+
`;
|
|
18593
|
+
case "snapshot":
|
|
18594
|
+
return `TEST STRATEGY: snapshot
|
|
18595
|
+
Render the component and use toMatchSnapshot() to capture and compare snapshots.
|
|
18596
|
+
|
|
18597
|
+
`;
|
|
18598
|
+
default:
|
|
18599
|
+
return "";
|
|
18600
|
+
}
|
|
18601
|
+
}
|
|
18521
18602
|
function parseAcceptanceCriteria(specContent) {
|
|
18522
18603
|
const criteria = [];
|
|
18523
18604
|
const lines = specContent.split(`
|
|
@@ -21034,7 +21115,7 @@ var package_default;
|
|
|
21034
21115
|
var init_package = __esm(() => {
|
|
21035
21116
|
package_default = {
|
|
21036
21117
|
name: "@nathapp/nax",
|
|
21037
|
-
version: "0.40.
|
|
21118
|
+
version: "0.40.1",
|
|
21038
21119
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21039
21120
|
type: "module",
|
|
21040
21121
|
bin: {
|
|
@@ -21098,8 +21179,8 @@ var init_version = __esm(() => {
|
|
|
21098
21179
|
NAX_VERSION = package_default.version;
|
|
21099
21180
|
NAX_COMMIT = (() => {
|
|
21100
21181
|
try {
|
|
21101
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
21102
|
-
return "
|
|
21182
|
+
if (/^[0-9a-f]{6,10}$/.test("ba3f634"))
|
|
21183
|
+
return "ba3f634";
|
|
21103
21184
|
} catch {}
|
|
21104
21185
|
try {
|
|
21105
21186
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -22846,7 +22927,9 @@ ${stderr}` };
|
|
|
22846
22927
|
refinedCriteria = await _acceptanceSetupDeps.refine(allCriteria, {
|
|
22847
22928
|
storyId: ctx.prd.userStories[0]?.id ?? "US-001",
|
|
22848
22929
|
codebaseContext: "",
|
|
22849
|
-
config: ctx.config
|
|
22930
|
+
config: ctx.config,
|
|
22931
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
22932
|
+
testFramework: ctx.config.acceptance.testFramework
|
|
22850
22933
|
});
|
|
22851
22934
|
} else {
|
|
22852
22935
|
refinedCriteria = allCriteria.map((c) => ({
|
|
@@ -22863,7 +22946,9 @@ ${stderr}` };
|
|
|
22863
22946
|
codebaseContext: "",
|
|
22864
22947
|
modelTier: ctx.config.acceptance.model ?? "fast",
|
|
22865
22948
|
modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
|
|
22866
|
-
config: ctx.config
|
|
22949
|
+
config: ctx.config,
|
|
22950
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
22951
|
+
testFramework: ctx.config.acceptance.testFramework
|
|
22867
22952
|
});
|
|
22868
22953
|
await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
|
|
22869
22954
|
}
|
package/package.json
CHANGED
|
@@ -82,6 +82,8 @@ export async function generateFromPRD(
|
|
|
82
82
|
|
|
83
83
|
const criteriaList = refinedCriteria.map((c, i) => `AC-${i + 1}: ${c.refined}`).join("\n");
|
|
84
84
|
|
|
85
|
+
const strategyInstructions = buildStrategyInstructions(options.testStrategy, options.testFramework);
|
|
86
|
+
|
|
85
87
|
const prompt = `You are a test engineer. Generate acceptance tests for the "${options.featureName}" feature based on the refined acceptance criteria below.
|
|
86
88
|
|
|
87
89
|
CODEBASE CONTEXT:
|
|
@@ -90,7 +92,7 @@ ${options.codebaseContext}
|
|
|
90
92
|
ACCEPTANCE CRITERIA (refined):
|
|
91
93
|
${criteriaList}
|
|
92
94
|
|
|
93
|
-
Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
|
|
95
|
+
${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
|
|
94
96
|
|
|
95
97
|
Use this structure:
|
|
96
98
|
|
|
@@ -127,6 +129,26 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
|
|
|
127
129
|
return { testCode, criteria };
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
function buildStrategyInstructions(strategy?: string, framework?: string): string {
|
|
133
|
+
switch (strategy) {
|
|
134
|
+
case "component": {
|
|
135
|
+
const fw = framework ?? "ink-testing-library";
|
|
136
|
+
if (fw === "react") {
|
|
137
|
+
return "TEST STRATEGY: component (react)\nImport render and screen from @testing-library/react. Render the component and use screen.getByText to assert on output.\n\n";
|
|
138
|
+
}
|
|
139
|
+
return "TEST STRATEGY: component (ink-testing-library)\nImport render from ink-testing-library. Render the component and use lastFrame() to assert on output.\n\n";
|
|
140
|
+
}
|
|
141
|
+
case "cli":
|
|
142
|
+
return "TEST STRATEGY: cli\nUse Bun.spawn to run the binary. Read stdout and assert on the text output.\n\n";
|
|
143
|
+
case "e2e":
|
|
144
|
+
return "TEST STRATEGY: e2e\nUse fetch() against http://localhost to call the running service. Assert on response body using response.text() or response.json().\n\n";
|
|
145
|
+
case "snapshot":
|
|
146
|
+
return "TEST STRATEGY: snapshot\nRender the component and use toMatchSnapshot() to capture and compare snapshots.\n\n";
|
|
147
|
+
default:
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
export function parseAcceptanceCriteria(specContent: string): AcceptanceCriterion[] {
|
|
131
153
|
const criteria: AcceptanceCriterion[] = [];
|
|
132
154
|
const lines = specContent.split("\n");
|
|
@@ -22,21 +22,38 @@ export const _refineDeps = {
|
|
|
22
22
|
adapter: new ClaudeCodeAdapter() as AgentAdapter,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Strategy-specific context for the refinement prompt.
|
|
27
|
+
*/
|
|
28
|
+
export interface RefinementPromptOptions {
|
|
29
|
+
/** Test strategy — controls strategy-specific prompt instructions */
|
|
30
|
+
testStrategy?: "unit" | "component" | "cli" | "e2e" | "snapshot";
|
|
31
|
+
/** Test framework — informs LLM which testing library syntax to use */
|
|
32
|
+
testFramework?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/**
|
|
26
36
|
* Build the LLM prompt for refining acceptance criteria.
|
|
27
37
|
*
|
|
28
38
|
* @param criteria - Raw AC strings from PRD
|
|
29
39
|
* @param codebaseContext - File tree / dependency context
|
|
40
|
+
* @param options - Optional strategy/framework context
|
|
30
41
|
* @returns Formatted prompt string
|
|
31
42
|
*/
|
|
32
|
-
export function buildRefinementPrompt(
|
|
43
|
+
export function buildRefinementPrompt(
|
|
44
|
+
criteria: string[],
|
|
45
|
+
codebaseContext: string,
|
|
46
|
+
options?: RefinementPromptOptions,
|
|
47
|
+
): string {
|
|
33
48
|
const criteriaList = criteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
49
|
+
const strategySection = buildStrategySection(options);
|
|
50
|
+
const refinedExample = buildRefinedExample(options?.testStrategy);
|
|
34
51
|
|
|
35
52
|
return `You are an acceptance criteria refinement assistant. Your task is to convert raw acceptance criteria into concrete, machine-verifiable assertions.
|
|
36
53
|
|
|
37
54
|
CODEBASE CONTEXT:
|
|
38
55
|
${codebaseContext}
|
|
39
|
-
|
|
56
|
+
${strategySection}
|
|
40
57
|
ACCEPTANCE CRITERIA TO REFINE:
|
|
41
58
|
${criteriaList}
|
|
42
59
|
|
|
@@ -51,12 +68,62 @@ Respond with ONLY a JSON array (no markdown code fences):
|
|
|
51
68
|
|
|
52
69
|
Rules:
|
|
53
70
|
- "original" must match the input criterion text exactly
|
|
54
|
-
- "refined" must be a concrete assertion (e.g.,
|
|
71
|
+
- "refined" must be a concrete assertion (e.g., ${refinedExample})
|
|
55
72
|
- "testable" is false only if the criterion cannot be automatically verified (e.g., "UX feels responsive", "design looks good")
|
|
56
73
|
- "storyId" leave as empty string — it will be assigned by the caller
|
|
57
74
|
- Respond with ONLY the JSON array`;
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Build strategy-specific instructions section for the prompt.
|
|
79
|
+
*/
|
|
80
|
+
function buildStrategySection(options?: RefinementPromptOptions): string {
|
|
81
|
+
if (!options?.testStrategy) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const framework = options.testFramework ? ` Use ${options.testFramework} testing library syntax.` : "";
|
|
86
|
+
|
|
87
|
+
switch (options.testStrategy) {
|
|
88
|
+
case "component":
|
|
89
|
+
return `
|
|
90
|
+
TEST STRATEGY: component
|
|
91
|
+
Focus assertions on rendered output visible on screen — text content, visible elements, and screen state.
|
|
92
|
+
Assert what the user sees rendered in the component, not what internal functions produce.${framework}
|
|
93
|
+
`;
|
|
94
|
+
case "cli":
|
|
95
|
+
return `
|
|
96
|
+
TEST STRATEGY: cli
|
|
97
|
+
Focus assertions on stdout and stderr text output from the CLI command.
|
|
98
|
+
Assert about terminal output content, exit codes, and standard output/standard error streams.${framework}
|
|
99
|
+
`;
|
|
100
|
+
case "e2e":
|
|
101
|
+
return `
|
|
102
|
+
TEST STRATEGY: e2e
|
|
103
|
+
Focus assertions on HTTP response content — status codes, response bodies, and endpoint behavior.
|
|
104
|
+
Assert about HTTP responses, status codes, and API endpoint output.${framework}
|
|
105
|
+
`;
|
|
106
|
+
default:
|
|
107
|
+
return framework ? `\nTEST FRAMEWORK: ${options.testFramework}\n` : "";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build the "refined" example string based on the test strategy.
|
|
113
|
+
*/
|
|
114
|
+
function buildRefinedExample(testStrategy?: RefinementPromptOptions["testStrategy"]): string {
|
|
115
|
+
switch (testStrategy) {
|
|
116
|
+
case "component":
|
|
117
|
+
return '"Text content visible on screen matches expected", "Rendered output contains expected element"';
|
|
118
|
+
case "cli":
|
|
119
|
+
return '"stdout contains expected text", "stderr is empty on success", "exit code is 0"';
|
|
120
|
+
case "e2e":
|
|
121
|
+
return '"HTTP status 200 returned", "Response body contains expected field", "Endpoint returns JSON"';
|
|
122
|
+
default:
|
|
123
|
+
return '"Array of length N returned", "HTTP status 200 returned"';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
60
127
|
/**
|
|
61
128
|
* Parse the LLM JSON response into RefinedCriterion[].
|
|
62
129
|
*
|
|
@@ -105,7 +172,7 @@ export async function refineAcceptanceCriteria(
|
|
|
105
172
|
return [];
|
|
106
173
|
}
|
|
107
174
|
|
|
108
|
-
const { storyId, codebaseContext, config } = context;
|
|
175
|
+
const { storyId, codebaseContext, config, testStrategy, testFramework } = context;
|
|
109
176
|
const logger = getLogger();
|
|
110
177
|
|
|
111
178
|
const modelTier = config.acceptance?.model ?? "fast";
|
|
@@ -116,7 +183,7 @@ export async function refineAcceptanceCriteria(
|
|
|
116
183
|
}
|
|
117
184
|
|
|
118
185
|
const modelDef = resolveModel(modelEntry);
|
|
119
|
-
const prompt = buildRefinementPrompt(criteria, codebaseContext);
|
|
186
|
+
const prompt = buildRefinementPrompt(criteria, codebaseContext, { testStrategy, testFramework });
|
|
120
187
|
|
|
121
188
|
let response: string;
|
|
122
189
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI test template builder
|
|
3
|
+
*
|
|
4
|
+
* Generates acceptance test structure for CLI testing strategy.
|
|
5
|
+
* Uses Bun.spawn to run the binary and asserts on stdout text.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AcceptanceCriterion } from "../types";
|
|
9
|
+
|
|
10
|
+
export interface CliTemplateOptions {
|
|
11
|
+
featureName: string;
|
|
12
|
+
criteria: AcceptanceCriterion[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build CLI test template code for the given criteria.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Feature name and criteria list
|
|
19
|
+
* @returns TypeScript test code string
|
|
20
|
+
*/
|
|
21
|
+
export function buildCliTemplate(options: CliTemplateOptions): string {
|
|
22
|
+
const { featureName, criteria } = options;
|
|
23
|
+
|
|
24
|
+
const tests = criteria
|
|
25
|
+
.map(
|
|
26
|
+
(ac) => ` test("${ac.id}: ${ac.text}", async () => {
|
|
27
|
+
const proc = Bun.spawn(["bun", "run", "src/${featureName}.ts"], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
const [exitCode, stdout] = await Promise.all([
|
|
32
|
+
proc.exited,
|
|
33
|
+
new Response(proc.stdout).text(),
|
|
34
|
+
]);
|
|
35
|
+
expect(exitCode).toBe(0);
|
|
36
|
+
expect(stdout).toContain(""); // Replace with expected stdout text
|
|
37
|
+
});`,
|
|
38
|
+
)
|
|
39
|
+
.join("\n\n");
|
|
40
|
+
|
|
41
|
+
return `import { describe, expect, test } from "bun:test";
|
|
42
|
+
|
|
43
|
+
describe("${featureName} - Acceptance Tests", () => {
|
|
44
|
+
${tests}
|
|
45
|
+
});
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component test template builder
|
|
3
|
+
*
|
|
4
|
+
* Generates acceptance test structure for component testing strategy.
|
|
5
|
+
* Supports ink-testing-library (lastFrame) and react (screen.getByText).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AcceptanceCriterion } from "../types";
|
|
9
|
+
|
|
10
|
+
export interface ComponentTemplateOptions {
|
|
11
|
+
featureName: string;
|
|
12
|
+
criteria: AcceptanceCriterion[];
|
|
13
|
+
/** Test framework: 'ink-testing-library' | 'react' */
|
|
14
|
+
testFramework?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build component test template code for the given criteria.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Feature name, criteria, and test framework
|
|
21
|
+
* @returns TypeScript test code string
|
|
22
|
+
*/
|
|
23
|
+
export function buildComponentTemplate(options: ComponentTemplateOptions): string {
|
|
24
|
+
const { featureName, criteria, testFramework = "ink-testing-library" } = options;
|
|
25
|
+
|
|
26
|
+
if (testFramework === "react") {
|
|
27
|
+
return buildReactTemplate(featureName, criteria);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return buildInkTemplate(featureName, criteria);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildInkTemplate(featureName: string, criteria: AcceptanceCriterion[]): string {
|
|
34
|
+
const tests = criteria
|
|
35
|
+
.map(
|
|
36
|
+
(ac) => ` test("${ac.id}: ${ac.text}", () => {
|
|
37
|
+
const { lastFrame } = render(<${toPascalCase(featureName)} />);
|
|
38
|
+
expect(lastFrame()).toContain(""); // Replace with expected output
|
|
39
|
+
});`,
|
|
40
|
+
)
|
|
41
|
+
.join("\n\n");
|
|
42
|
+
|
|
43
|
+
return `import { describe, expect, test } from "bun:test";
|
|
44
|
+
import { render } from "ink-testing-library";
|
|
45
|
+
import { ${toPascalCase(featureName)} } from "../src/${featureName}";
|
|
46
|
+
|
|
47
|
+
describe("${featureName} - Acceptance Tests", () => {
|
|
48
|
+
${tests}
|
|
49
|
+
});
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildReactTemplate(featureName: string, criteria: AcceptanceCriterion[]): string {
|
|
54
|
+
const tests = criteria
|
|
55
|
+
.map(
|
|
56
|
+
(ac) => ` test("${ac.id}: ${ac.text}", () => {
|
|
57
|
+
render(<${toPascalCase(featureName)} />);
|
|
58
|
+
expect(screen.getByText("")).toBeTruthy(); // Replace with expected text
|
|
59
|
+
});`,
|
|
60
|
+
)
|
|
61
|
+
.join("\n\n");
|
|
62
|
+
|
|
63
|
+
return `import { describe, expect, test } from "bun:test";
|
|
64
|
+
import { render, screen } from "@testing-library/react";
|
|
65
|
+
import { ${toPascalCase(featureName)} } from "../src/${featureName}";
|
|
66
|
+
|
|
67
|
+
describe("${featureName} - Acceptance Tests", () => {
|
|
68
|
+
${tests}
|
|
69
|
+
});
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toPascalCase(name: string): string {
|
|
74
|
+
return name
|
|
75
|
+
.split(/[-_\s]+/)
|
|
76
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E test template builder
|
|
3
|
+
*
|
|
4
|
+
* Generates acceptance test structure for end-to-end testing strategy.
|
|
5
|
+
* Uses fetch() against localhost and asserts on response body.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AcceptanceCriterion } from "../types";
|
|
9
|
+
|
|
10
|
+
export interface E2eTemplateOptions {
|
|
11
|
+
featureName: string;
|
|
12
|
+
criteria: AcceptanceCriterion[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PORT = 3000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build E2E test template code for the given criteria.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Feature name and criteria list
|
|
21
|
+
* @returns TypeScript test code string
|
|
22
|
+
*/
|
|
23
|
+
export function buildE2eTemplate(options: E2eTemplateOptions): string {
|
|
24
|
+
const { featureName, criteria } = options;
|
|
25
|
+
|
|
26
|
+
const tests = criteria
|
|
27
|
+
.map(
|
|
28
|
+
(ac) => ` test("${ac.id}: ${ac.text}", async () => {
|
|
29
|
+
const response = await fetch("http://localhost:${DEFAULT_PORT}/api/${featureName}");
|
|
30
|
+
expect(response.ok).toBe(true);
|
|
31
|
+
const body = await response.text();
|
|
32
|
+
expect(body).toContain(""); // Replace with expected response body
|
|
33
|
+
});`,
|
|
34
|
+
)
|
|
35
|
+
.join("\n\n");
|
|
36
|
+
|
|
37
|
+
return `import { describe, expect, test } from "bun:test";
|
|
38
|
+
|
|
39
|
+
describe("${featureName} - Acceptance Tests", () => {
|
|
40
|
+
${tests}
|
|
41
|
+
});
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance Test Template Builders
|
|
3
|
+
*
|
|
4
|
+
* One builder per test strategy. The generator selects the appropriate
|
|
5
|
+
* builder based on the testStrategy option.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { buildUnitTemplate } from "./unit";
|
|
9
|
+
export type { UnitTemplateOptions } from "./unit";
|
|
10
|
+
|
|
11
|
+
export { buildComponentTemplate } from "./component";
|
|
12
|
+
export type { ComponentTemplateOptions } from "./component";
|
|
13
|
+
|
|
14
|
+
export { buildCliTemplate } from "./cli";
|
|
15
|
+
export type { CliTemplateOptions } from "./cli";
|
|
16
|
+
|
|
17
|
+
export { buildE2eTemplate } from "./e2e";
|
|
18
|
+
export type { E2eTemplateOptions } from "./e2e";
|
|
19
|
+
|
|
20
|
+
export { buildSnapshotTemplate } from "./snapshot";
|
|
21
|
+
export type { SnapshotTemplateOptions } from "./snapshot";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot test template builder
|
|
3
|
+
*
|
|
4
|
+
* Generates acceptance test structure for snapshot testing strategy.
|
|
5
|
+
* Renders the component and uses toMatchSnapshot() for assertions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AcceptanceCriterion } from "../types";
|
|
9
|
+
|
|
10
|
+
export interface SnapshotTemplateOptions {
|
|
11
|
+
featureName: string;
|
|
12
|
+
criteria: AcceptanceCriterion[];
|
|
13
|
+
/** Test framework: 'ink-testing-library' | 'react' */
|
|
14
|
+
testFramework?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build snapshot test template code for the given criteria.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Feature name, criteria, and optional test framework
|
|
21
|
+
* @returns TypeScript test code string
|
|
22
|
+
*/
|
|
23
|
+
export function buildSnapshotTemplate(options: SnapshotTemplateOptions): string {
|
|
24
|
+
const { featureName, criteria } = options;
|
|
25
|
+
|
|
26
|
+
const tests = criteria
|
|
27
|
+
.map(
|
|
28
|
+
(ac) => ` test("${ac.id}: ${ac.text}", () => {
|
|
29
|
+
const { lastFrame } = render(<${toPascalCase(featureName)} />);
|
|
30
|
+
expect(lastFrame()).toMatchSnapshot();
|
|
31
|
+
});`,
|
|
32
|
+
)
|
|
33
|
+
.join("\n\n");
|
|
34
|
+
|
|
35
|
+
return `import { describe, expect, test } from "bun:test";
|
|
36
|
+
import { render } from "ink-testing-library";
|
|
37
|
+
import { ${toPascalCase(featureName)} } from "../src/${featureName}";
|
|
38
|
+
|
|
39
|
+
describe("${featureName} - Acceptance Tests", () => {
|
|
40
|
+
${tests}
|
|
41
|
+
});
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toPascalCase(name: string): string {
|
|
46
|
+
return name
|
|
47
|
+
.split(/[-_\s]+/)
|
|
48
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
49
|
+
.join("");
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test template builder
|
|
3
|
+
*
|
|
4
|
+
* Generates acceptance test structure for unit testing strategy:
|
|
5
|
+
* imports the function under test, calls it, and asserts on the return value.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AcceptanceCriterion } from "../types";
|
|
9
|
+
|
|
10
|
+
export interface UnitTemplateOptions {
|
|
11
|
+
featureName: string;
|
|
12
|
+
criteria: AcceptanceCriterion[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build unit test template code for the given criteria.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Feature name and criteria list
|
|
19
|
+
* @returns TypeScript test code string
|
|
20
|
+
*/
|
|
21
|
+
export function buildUnitTemplate(options: UnitTemplateOptions): string {
|
|
22
|
+
const { featureName, criteria } = options;
|
|
23
|
+
|
|
24
|
+
const tests = criteria
|
|
25
|
+
.map(
|
|
26
|
+
(ac) => ` test("${ac.id}: ${ac.text}", async () => {
|
|
27
|
+
// TODO: import and call the function under test
|
|
28
|
+
expect(true).toBe(true); // Replace with real assertion
|
|
29
|
+
});`,
|
|
30
|
+
)
|
|
31
|
+
.join("\n\n");
|
|
32
|
+
|
|
33
|
+
return `import { describe, expect, test } from "bun:test";
|
|
34
|
+
import { ${toCamelCase(featureName)} } from "../src/${toKebabCase(featureName)}";
|
|
35
|
+
|
|
36
|
+
describe("${featureName} - Acceptance Tests", () => {
|
|
37
|
+
${tests}
|
|
38
|
+
});
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toCamelCase(name: string): string {
|
|
43
|
+
return name.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toKebabCase(name: string): string {
|
|
47
|
+
return name.toLowerCase().replace(/\s+/g, "-");
|
|
48
|
+
}
|
package/src/acceptance/types.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Types for generating acceptance tests from spec.md acceptance criteria.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ModelDef, ModelTier, NaxConfig } from "../config/schema";
|
|
7
|
+
import type { AcceptanceTestStrategy, ModelDef, ModelTier, NaxConfig } from "../config/schema";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A single refined acceptance criterion produced by the refinement module.
|
|
@@ -30,6 +30,10 @@ export interface RefinementContext {
|
|
|
30
30
|
codebaseContext: string;
|
|
31
31
|
/** Global config — model tier resolved from config.acceptance.model */
|
|
32
32
|
config: NaxConfig;
|
|
33
|
+
/** Test strategy — controls strategy-specific prompt instructions */
|
|
34
|
+
testStrategy?: AcceptanceTestStrategy;
|
|
35
|
+
/** Test framework — informs LLM which testing library syntax to use */
|
|
36
|
+
testFramework?: string;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -84,6 +88,10 @@ export interface GenerateFromPRDOptions {
|
|
|
84
88
|
modelDef: ModelDef;
|
|
85
89
|
/** Global config for quality settings */
|
|
86
90
|
config: NaxConfig;
|
|
91
|
+
/** Test strategy to use for template selection (default: 'unit') */
|
|
92
|
+
testStrategy?: AcceptanceTestStrategy;
|
|
93
|
+
/** Test framework for component/snapshot strategies (e.g. 'ink-testing-library', 'react') */
|
|
94
|
+
testFramework?: string;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
export interface GenerateAcceptanceTestsOptions {
|
package/src/cli/init-detect.ts
CHANGED
|
@@ -5,12 +5,74 @@
|
|
|
5
5
|
* for nax/config.json.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
11
|
/** Detected project runtime */
|
|
12
12
|
export type Runtime = "bun" | "node" | "unknown";
|
|
13
13
|
|
|
14
|
+
/** Detected UI framework */
|
|
15
|
+
export type UIFramework = "ink" | "react" | "vue" | "svelte";
|
|
16
|
+
|
|
17
|
+
/** Full stack info including UI framework and bin detection */
|
|
18
|
+
export interface StackInfo extends ProjectStack {
|
|
19
|
+
uiFramework?: UIFramework;
|
|
20
|
+
hasBin?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Shape of a parsed package.json for detection purposes */
|
|
24
|
+
interface PackageJson {
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
devDependencies?: Record<string, string>;
|
|
27
|
+
peerDependencies?: Record<string, string>;
|
|
28
|
+
bin?: Record<string, string> | string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readPackageJson(projectRoot: string): PackageJson | undefined {
|
|
32
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
33
|
+
if (!existsSync(pkgPath)) return undefined;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson;
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function allDeps(pkg: PackageJson): Record<string, string> {
|
|
42
|
+
return {
|
|
43
|
+
...pkg.dependencies,
|
|
44
|
+
...pkg.devDependencies,
|
|
45
|
+
...pkg.peerDependencies,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detectUIFramework(pkg: PackageJson): UIFramework | undefined {
|
|
50
|
+
const deps = allDeps(pkg);
|
|
51
|
+
if ("ink" in deps) return "ink";
|
|
52
|
+
if ("react" in deps || "next" in deps) return "react";
|
|
53
|
+
if ("vue" in deps || "nuxt" in deps) return "vue";
|
|
54
|
+
if ("svelte" in deps || "@sveltejs/kit" in deps) return "svelte";
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detectHasBin(pkg: PackageJson): boolean {
|
|
59
|
+
return pkg.bin !== undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Detect the project stack including UI framework from package.json.
|
|
64
|
+
*/
|
|
65
|
+
export function detectStack(projectRoot: string): StackInfo {
|
|
66
|
+
const base = detectProjectStack(projectRoot);
|
|
67
|
+
const pkg = readPackageJson(projectRoot);
|
|
68
|
+
if (!pkg) return base;
|
|
69
|
+
return {
|
|
70
|
+
...base,
|
|
71
|
+
uiFramework: detectUIFramework(pkg),
|
|
72
|
+
hasBin: detectHasBin(pkg) || undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
14
76
|
/** Detected project language */
|
|
15
77
|
export type Language = "typescript" | "python" | "rust" | "go" | "unknown";
|
|
16
78
|
|
|
@@ -146,24 +208,48 @@ function isStackDetected(stack: ProjectStack): boolean {
|
|
|
146
208
|
return stack.runtime !== "unknown" || stack.language !== "unknown";
|
|
147
209
|
}
|
|
148
210
|
|
|
211
|
+
/** Build the acceptance config section from StackInfo, or undefined if not applicable. */
|
|
212
|
+
function buildAcceptanceConfig(stack: StackInfo): { testStrategy: string; testFramework?: string } | undefined {
|
|
213
|
+
if (stack.uiFramework === "ink") {
|
|
214
|
+
return { testStrategy: "component", testFramework: "ink-testing-library" };
|
|
215
|
+
}
|
|
216
|
+
if (stack.uiFramework === "react") {
|
|
217
|
+
return { testStrategy: "component", testFramework: "@testing-library/react" };
|
|
218
|
+
}
|
|
219
|
+
if (stack.uiFramework === "vue") {
|
|
220
|
+
return { testStrategy: "component", testFramework: "@testing-library/vue" };
|
|
221
|
+
}
|
|
222
|
+
if (stack.uiFramework === "svelte") {
|
|
223
|
+
return { testStrategy: "component", testFramework: "@testing-library/svelte" };
|
|
224
|
+
}
|
|
225
|
+
if (stack.hasBin) {
|
|
226
|
+
const testFramework = stack.runtime === "bun" ? "bun:test" : "jest";
|
|
227
|
+
return { testStrategy: "cli", testFramework };
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
149
232
|
/**
|
|
150
233
|
* Build the full init config object from a detected project stack.
|
|
151
234
|
* Falls back to minimal config when stack is undetected.
|
|
152
235
|
*/
|
|
153
|
-
export function buildInitConfig(stack: ProjectStack): object {
|
|
236
|
+
export function buildInitConfig(stack: ProjectStack | StackInfo): object {
|
|
237
|
+
const stackInfo = stack as StackInfo;
|
|
238
|
+
const acceptance = buildAcceptanceConfig(stackInfo);
|
|
239
|
+
|
|
154
240
|
if (!isStackDetected(stack)) {
|
|
155
|
-
return { version: 1 };
|
|
241
|
+
return acceptance ? { version: 1, acceptance } : { version: 1 };
|
|
156
242
|
}
|
|
157
243
|
|
|
158
244
|
const commands = buildQualityCommands(stack);
|
|
159
245
|
const hasCommands = Object.keys(commands).length > 0;
|
|
160
246
|
|
|
161
|
-
if (!hasCommands) {
|
|
247
|
+
if (!hasCommands && !acceptance) {
|
|
162
248
|
return { version: 1 };
|
|
163
249
|
}
|
|
164
250
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
251
|
+
const config: Record<string, unknown> = { version: 1 };
|
|
252
|
+
if (hasCommands) config.quality = { commands };
|
|
253
|
+
if (acceptance) config.acceptance = acceptance;
|
|
254
|
+
return config;
|
|
169
255
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { globalConfigDir, projectConfigDir } from "../config/paths";
|
|
11
11
|
import { getLogger } from "../logger";
|
|
12
12
|
import { initContext } from "./init-context";
|
|
13
|
-
import { buildInitConfig,
|
|
13
|
+
import { buildInitConfig, detectStack } from "./init-detect";
|
|
14
14
|
import type { ProjectStack } from "./init-detect";
|
|
15
15
|
import { promptsInitCommand } from "./prompts";
|
|
16
16
|
|
|
@@ -183,7 +183,7 @@ export async function initProject(projectRoot: string, options?: InitProjectOpti
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// Detect project stack and build config
|
|
186
|
-
const stack =
|
|
186
|
+
const stack = detectStack(projectRoot);
|
|
187
187
|
const projectConfig = buildInitConfig(stack);
|
|
188
188
|
logger.info("init", "Detected project stack", {
|
|
189
189
|
runtime: stack.runtime,
|
package/src/config/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type {
|
|
|
14
14
|
TierConfig,
|
|
15
15
|
RectificationConfig,
|
|
16
16
|
} from "./schema";
|
|
17
|
-
export { DEFAULT_CONFIG, resolveModel, NaxConfigSchema } from "./schema";
|
|
17
|
+
export { DEFAULT_CONFIG, resolveModel, NaxConfigSchema, AcceptanceConfigSchema } from "./schema";
|
|
18
18
|
export { loadConfig, findProjectDir, globalConfigPath } from "./loader";
|
|
19
19
|
export { validateConfig, type ValidationResult } from "./validate"; // @deprecated: Use NaxConfigSchema.safeParse() instead
|
|
20
20
|
export { validateDirectory, validateFilePath, isWithinDirectory, MAX_DIRECTORY_DEPTH } from "./path-security";
|
|
@@ -228,6 +228,9 @@ export interface PlanConfig {
|
|
|
228
228
|
outputPath: string;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/** Valid test strategy values for acceptance testing */
|
|
232
|
+
export type AcceptanceTestStrategy = "unit" | "component" | "cli" | "e2e" | "snapshot";
|
|
233
|
+
|
|
231
234
|
/** Acceptance validation config */
|
|
232
235
|
export interface AcceptanceConfig {
|
|
233
236
|
/** Enable acceptance test generation and validation */
|
|
@@ -244,6 +247,10 @@ export interface AcceptanceConfig {
|
|
|
244
247
|
refinement: boolean;
|
|
245
248
|
/** Whether to run RED gate check after generating acceptance tests (default: true) */
|
|
246
249
|
redGate: boolean;
|
|
250
|
+
/** Test strategy for acceptance tests (default: auto-detect) */
|
|
251
|
+
testStrategy?: AcceptanceTestStrategy;
|
|
252
|
+
/** Test framework for acceptance tests (default: auto-detect) */
|
|
253
|
+
testFramework?: string;
|
|
247
254
|
}
|
|
248
255
|
|
|
249
256
|
/** Optimizer config (v0.10) */
|
package/src/config/schema.ts
CHANGED
|
@@ -30,6 +30,7 @@ export type {
|
|
|
30
30
|
ReviewConfig,
|
|
31
31
|
PlanConfig,
|
|
32
32
|
AcceptanceConfig,
|
|
33
|
+
AcceptanceTestStrategy,
|
|
33
34
|
OptimizerConfig,
|
|
34
35
|
PluginConfigEntry,
|
|
35
36
|
HooksConfig,
|
|
@@ -52,7 +53,7 @@ export type {
|
|
|
52
53
|
export { resolveModel } from "./types";
|
|
53
54
|
|
|
54
55
|
// Zod schemas
|
|
55
|
-
export { NaxConfigSchema } from "./schemas";
|
|
56
|
+
export { NaxConfigSchema, AcceptanceConfigSchema } from "./schemas";
|
|
56
57
|
|
|
57
58
|
// Default config
|
|
58
59
|
export { DEFAULT_CONFIG } from "./defaults";
|
package/src/config/schemas.ts
CHANGED
|
@@ -210,7 +210,7 @@ const PlanConfigSchema = z.object({
|
|
|
210
210
|
outputPath: z.string().min(1, "plan.outputPath must be non-empty"),
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
-
const AcceptanceConfigSchema = z.object({
|
|
213
|
+
export const AcceptanceConfigSchema = z.object({
|
|
214
214
|
enabled: z.boolean(),
|
|
215
215
|
maxRetries: z.number().int().nonnegative(),
|
|
216
216
|
generateTests: z.boolean(),
|
|
@@ -218,6 +218,8 @@ const AcceptanceConfigSchema = z.object({
|
|
|
218
218
|
model: z.enum(["fast", "balanced", "powerful"]).default("fast"),
|
|
219
219
|
refinement: z.boolean().default(true),
|
|
220
220
|
redGate: z.boolean().default(true),
|
|
221
|
+
testStrategy: z.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
|
|
222
|
+
testFramework: z.string().min(1, "acceptance.testFramework must be non-empty").optional(),
|
|
221
223
|
});
|
|
222
224
|
|
|
223
225
|
const TestCoverageConfigSchema = z.object({
|
package/src/config/types.ts
CHANGED
|
@@ -89,6 +89,8 @@ export const acceptanceSetupStage: PipelineStage = {
|
|
|
89
89
|
storyId: ctx.prd.userStories[0]?.id ?? "US-001",
|
|
90
90
|
codebaseContext: "",
|
|
91
91
|
config: ctx.config,
|
|
92
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
93
|
+
testFramework: ctx.config.acceptance.testFramework,
|
|
92
94
|
});
|
|
93
95
|
} else {
|
|
94
96
|
refinedCriteria = allCriteria.map((c) => ({
|
|
@@ -108,6 +110,8 @@ export const acceptanceSetupStage: PipelineStage = {
|
|
|
108
110
|
modelTier: ctx.config.acceptance.model ?? "fast",
|
|
109
111
|
modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
|
|
110
112
|
config: ctx.config,
|
|
113
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
114
|
+
testFramework: ctx.config.acceptance.testFramework,
|
|
111
115
|
});
|
|
112
116
|
|
|
113
117
|
await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
|