@nathapp/nax 0.40.0 → 0.41.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/dist/nax.js +1166 -277
- package/package.json +2 -2
- package/src/acceptance/fix-generator.ts +4 -35
- package/src/acceptance/generator.ts +27 -28
- 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/agents/acp/adapter.ts +644 -0
- package/src/agents/acp/cost.ts +79 -0
- package/src/agents/acp/index.ts +9 -0
- package/src/agents/acp/interaction-bridge.ts +126 -0
- package/src/agents/acp/parser.ts +166 -0
- package/src/agents/acp/spawn-client.ts +309 -0
- package/src/agents/acp/types.ts +22 -0
- package/src/agents/claude-complete.ts +3 -3
- package/src/agents/registry.ts +83 -0
- package/src/agents/types-extended.ts +23 -0
- package/src/agents/types.ts +17 -0
- package/src/cli/analyze.ts +6 -2
- package/src/cli/init-detect.ts +94 -8
- package/src/cli/init.ts +2 -2
- package/src/cli/plan.ts +23 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +1 -1
- package/src/config/runtime-types.ts +17 -0
- package/src/config/schema.ts +3 -1
- package/src/config/schemas.ts +9 -1
- package/src/config/types.ts +2 -0
- package/src/execution/executor-types.ts +6 -0
- package/src/execution/iteration-runner.ts +2 -0
- package/src/execution/lifecycle/acceptance-loop.ts +5 -2
- package/src/execution/lifecycle/run-initialization.ts +16 -4
- package/src/execution/lifecycle/run-setup.ts +4 -0
- package/src/execution/runner-completion.ts +11 -1
- package/src/execution/runner-execution.ts +8 -0
- package/src/execution/runner-setup.ts +4 -0
- package/src/execution/runner.ts +10 -0
- package/src/pipeline/stages/acceptance-setup.ts +4 -0
- package/src/pipeline/stages/execution.ts +33 -1
- package/src/pipeline/stages/routing.ts +18 -7
- package/src/pipeline/types.ts +10 -0
- package/src/tdd/orchestrator.ts +7 -0
- package/src/tdd/rectification-gate.ts +6 -0
- package/src/tdd/session-runner.ts +4 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI Coding Agent Orchestrator
|
|
3
|
+
"version": "0.41.0",
|
|
4
|
+
"description": "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nax": "./dist/nax.js"
|
|
@@ -199,7 +199,7 @@ export async function generateFixStories(
|
|
|
199
199
|
adapter: AgentAdapter,
|
|
200
200
|
options: GenerateFixStoriesOptions,
|
|
201
201
|
): Promise<FixStory[]> {
|
|
202
|
-
const { failedACs, testOutput, prd, specContent,
|
|
202
|
+
const { failedACs, testOutput, prd, specContent, modelDef } = options;
|
|
203
203
|
|
|
204
204
|
const fixStories: FixStory[] = [];
|
|
205
205
|
|
|
@@ -225,42 +225,11 @@ export async function generateFixStories(
|
|
|
225
225
|
const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
|
|
226
226
|
|
|
227
227
|
try {
|
|
228
|
-
// Call
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const cmd = [adapter.binary, "--model", modelDef.model, ...permArgs, "-p", prompt];
|
|
232
|
-
|
|
233
|
-
const proc = Bun.spawn(cmd, {
|
|
234
|
-
cwd: workdir,
|
|
235
|
-
stdout: "pipe",
|
|
236
|
-
stderr: "pipe",
|
|
237
|
-
env: {
|
|
238
|
-
...process.env,
|
|
239
|
-
...(modelDef.env || {}),
|
|
240
|
-
},
|
|
228
|
+
// Call adapter to generate fix description
|
|
229
|
+
const fixDescription = await adapter.complete(prompt, {
|
|
230
|
+
model: modelDef.model,
|
|
241
231
|
});
|
|
242
232
|
|
|
243
|
-
const exitCode = await proc.exited;
|
|
244
|
-
const stdout = await new Response(proc.stdout).text();
|
|
245
|
-
const stderr = await new Response(proc.stderr).text();
|
|
246
|
-
|
|
247
|
-
if (exitCode !== 0) {
|
|
248
|
-
logger.warn("acceptance", "⚠ Agent fix generation failed", { failedAC, stderr });
|
|
249
|
-
// Use fallback description
|
|
250
|
-
fixStories.push({
|
|
251
|
-
id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
|
|
252
|
-
title: `Fix: ${failedAC}`,
|
|
253
|
-
failedAC,
|
|
254
|
-
testOutput,
|
|
255
|
-
relatedStories,
|
|
256
|
-
description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`,
|
|
257
|
-
});
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Extract fix description
|
|
262
|
-
const fixDescription = stdout.trim();
|
|
263
|
-
|
|
264
233
|
fixStories.push({
|
|
265
234
|
id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
|
|
266
235
|
title: `Fix: ${failedAC} — ${acText.slice(0, 50)}`,
|
|
@@ -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");
|
|
@@ -273,36 +295,13 @@ export async function generateAcceptanceTests(
|
|
|
273
295
|
const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
|
|
274
296
|
|
|
275
297
|
try {
|
|
276
|
-
// Call
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
const cmd = [adapter.binary, "--model", options.modelDef.model, ...permArgs, "-p", prompt];
|
|
280
|
-
|
|
281
|
-
const proc = Bun.spawn(cmd, {
|
|
282
|
-
cwd: options.workdir,
|
|
283
|
-
stdout: "pipe",
|
|
284
|
-
stderr: "pipe",
|
|
285
|
-
env: {
|
|
286
|
-
...process.env,
|
|
287
|
-
...(options.modelDef.env || {}),
|
|
288
|
-
},
|
|
298
|
+
// Call adapter to generate tests
|
|
299
|
+
const output = await adapter.complete(prompt, {
|
|
300
|
+
model: options.modelDef.model,
|
|
289
301
|
});
|
|
290
302
|
|
|
291
|
-
const exitCode = await proc.exited;
|
|
292
|
-
const stdout = await new Response(proc.stdout).text();
|
|
293
|
-
const stderr = await new Response(proc.stderr).text();
|
|
294
|
-
|
|
295
|
-
if (exitCode !== 0) {
|
|
296
|
-
logger.warn("acceptance", "⚠ Agent test generation failed", { stderr });
|
|
297
|
-
// Fall back to skeleton
|
|
298
|
-
return {
|
|
299
|
-
testCode: generateSkeletonTests(options.featureName, criteria),
|
|
300
|
-
criteria,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
303
|
// Extract test code from output
|
|
305
|
-
const testCode = extractTestCode(
|
|
304
|
+
const testCode = extractTestCode(output);
|
|
306
305
|
|
|
307
306
|
return {
|
|
308
307
|
testCode,
|
|
@@ -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 {
|