@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.
Files changed (49) hide show
  1. package/dist/nax.js +1166 -277
  2. package/package.json +2 -2
  3. package/src/acceptance/fix-generator.ts +4 -35
  4. package/src/acceptance/generator.ts +27 -28
  5. package/src/acceptance/refinement.ts +72 -5
  6. package/src/acceptance/templates/cli.ts +47 -0
  7. package/src/acceptance/templates/component.ts +78 -0
  8. package/src/acceptance/templates/e2e.ts +43 -0
  9. package/src/acceptance/templates/index.ts +21 -0
  10. package/src/acceptance/templates/snapshot.ts +50 -0
  11. package/src/acceptance/templates/unit.ts +48 -0
  12. package/src/acceptance/types.ts +9 -1
  13. package/src/agents/acp/adapter.ts +644 -0
  14. package/src/agents/acp/cost.ts +79 -0
  15. package/src/agents/acp/index.ts +9 -0
  16. package/src/agents/acp/interaction-bridge.ts +126 -0
  17. package/src/agents/acp/parser.ts +166 -0
  18. package/src/agents/acp/spawn-client.ts +309 -0
  19. package/src/agents/acp/types.ts +22 -0
  20. package/src/agents/claude-complete.ts +3 -3
  21. package/src/agents/registry.ts +83 -0
  22. package/src/agents/types-extended.ts +23 -0
  23. package/src/agents/types.ts +17 -0
  24. package/src/cli/analyze.ts +6 -2
  25. package/src/cli/init-detect.ts +94 -8
  26. package/src/cli/init.ts +2 -2
  27. package/src/cli/plan.ts +23 -0
  28. package/src/config/defaults.ts +1 -0
  29. package/src/config/index.ts +1 -1
  30. package/src/config/runtime-types.ts +17 -0
  31. package/src/config/schema.ts +3 -1
  32. package/src/config/schemas.ts +9 -1
  33. package/src/config/types.ts +2 -0
  34. package/src/execution/executor-types.ts +6 -0
  35. package/src/execution/iteration-runner.ts +2 -0
  36. package/src/execution/lifecycle/acceptance-loop.ts +5 -2
  37. package/src/execution/lifecycle/run-initialization.ts +16 -4
  38. package/src/execution/lifecycle/run-setup.ts +4 -0
  39. package/src/execution/runner-completion.ts +11 -1
  40. package/src/execution/runner-execution.ts +8 -0
  41. package/src/execution/runner-setup.ts +4 -0
  42. package/src/execution/runner.ts +10 -0
  43. package/src/pipeline/stages/acceptance-setup.ts +4 -0
  44. package/src/pipeline/stages/execution.ts +33 -1
  45. package/src/pipeline/stages/routing.ts +18 -7
  46. package/src/pipeline/types.ts +10 -0
  47. package/src/tdd/orchestrator.ts +7 -0
  48. package/src/tdd/rectification-gate.ts +6 -0
  49. 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.40.0",
4
- "description": "AI Coding Agent Orchestrator loops until done",
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, workdir, modelDef } = options;
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 agent to generate fix description
229
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
230
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
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 agent to generate tests (using decompose as pattern)
277
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
278
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
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(stdout);
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(criteria: string[], codebaseContext: string): string {
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., "Function returns array of length N", "HTTP status 200 returned")
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
+ }
@@ -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 {