@nathapp/nax 0.50.2 → 0.50.3

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 CHANGED
@@ -17874,7 +17874,8 @@ var init_schemas3 = __esm(() => {
17874
17874
  refinement: exports_external.boolean().default(true),
17875
17875
  redGate: exports_external.boolean().default(true),
17876
17876
  testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
17877
- testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional()
17877
+ testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional(),
17878
+ timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000)
17878
17879
  });
17879
17880
  TestCoverageConfigSchema = exports_external.object({
17880
17881
  enabled: exports_external.boolean().default(true),
@@ -18163,7 +18164,8 @@ var init_defaults = __esm(() => {
18163
18164
  testPath: "acceptance.test.ts",
18164
18165
  model: "fast",
18165
18166
  refinement: true,
18166
- redGate: true
18167
+ redGate: true,
18168
+ timeoutMs: 1800000
18167
18169
  },
18168
18170
  context: {
18169
18171
  fileInjection: "disabled",
@@ -18725,32 +18727,48 @@ async function generateFromPRD(_stories, refinedCriteria, options) {
18725
18727
  }
18726
18728
  const criteriaList = refinedCriteria.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
18727
18729
  `);
18728
- const strategyInstructions = buildStrategyInstructions(options.testStrategy, options.testFramework);
18729
- const prompt = `You are a test engineer. Generate acceptance tests for the "${options.featureName}" feature based on the refined acceptance criteria below.
18730
+ const frameworkOverrideLine = options.testFramework ? `
18731
+ [FRAMEWORK OVERRIDE: Use ${options.testFramework} as the test framework regardless of what you detect.]` : "";
18732
+ const basePrompt = `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${options.featureName}" feature.
18730
18733
 
18731
- CODEBASE CONTEXT:
18732
- ${options.codebaseContext}
18734
+ ## Step 1: Understand and Classify the Acceptance Criteria
18733
18735
 
18734
- ACCEPTANCE CRITERIA (refined):
18736
+ Read each AC below and classify its verification type:
18737
+ - **file-check**: Verify by reading source files (e.g. "no @nestjs/jwt imports", "file exists", "module registered", "uses registerAs pattern")
18738
+ - **runtime-check**: Load and invoke code directly, assert on return values or behavior
18739
+ - **integration-check**: Requires a running service (e.g. HTTP endpoint returns 200, 11th request returns 429, database query succeeds)
18740
+
18741
+ ACCEPTANCE CRITERIA:
18735
18742
  ${criteriaList}
18736
18743
 
18737
- ${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
18744
+ ## Step 2: Explore the Project
18738
18745
 
18739
- Structure example (do NOT wrap in markdown fences \u2014 output raw TypeScript only):
18746
+ Before writing any tests, examine the project to understand:
18747
+ 1. **Language and test framework** \u2014 check dependency manifests (package.json, go.mod, Gemfile, pyproject.toml, Cargo.toml, build.gradle, etc.) to identify the language and test runner
18748
+ 2. **Existing test patterns** \u2014 read 1-2 existing test files to understand import style, describe/test/it conventions, and available helpers
18749
+ 3. **Project structure** \u2014 identify relevant source directories to determine correct import or load paths
18740
18750
 
18741
- import { describe, test, expect } from "bun:test";
18751
+ ${frameworkOverrideLine}
18742
18752
 
18743
- describe("${options.featureName} - Acceptance Tests", () => {
18744
- test("AC-1: <description>", async () => {
18745
- // Test implementation
18746
- });
18747
- });
18753
+ ## Step 3: Generate the Acceptance Test File
18748
18754
 
18749
- IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\`\`typescript or \`\`\`). Start directly with the import statement.`;
18755
+ Write the complete acceptance test file using the framework identified in Step 2.
18756
+
18757
+ Rules:
18758
+ - **One test per AC**, named exactly "AC-N: <description>"
18759
+ - **file-check ACs** \u2192 read source files using the language's standard file I/O, assert with string or regex checks. Do not start the application.
18760
+ - **runtime-check ACs** \u2192 load or import the module directly and invoke it, assert on the return value or observable side effects
18761
+ - **integration-check ACs** \u2192 use the language's HTTP client or existing test helpers; add a clear setup block (beforeAll/setup/TestMain/etc.) explaining what must be running
18762
+ - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18763
+ - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18764
+ - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration`;
18765
+ const prompt = basePrompt;
18750
18766
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
18751
- const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
18767
+ const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
18752
18768
  model: options.modelDef.model,
18753
- config: options.config
18769
+ config: options.config,
18770
+ timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
18771
+ workdir: options.workdir
18754
18772
  });
18755
18773
  const testCode = extractTestCode(rawOutput);
18756
18774
  if (!testCode) {
@@ -18774,40 +18792,6 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
18774
18792
  await _generatorPRDDeps.writeFile(join2(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
18775
18793
  return { testCode, criteria };
18776
18794
  }
18777
- function buildStrategyInstructions(strategy, framework) {
18778
- switch (strategy) {
18779
- case "component": {
18780
- const fw = framework ?? "ink-testing-library";
18781
- if (fw === "react") {
18782
- return `TEST STRATEGY: component (react)
18783
- Import render and screen from @testing-library/react. Render the component and use screen.getByText to assert on output.
18784
-
18785
- `;
18786
- }
18787
- return `TEST STRATEGY: component (ink-testing-library)
18788
- Import render from ink-testing-library. Render the component and use lastFrame() to assert on output.
18789
-
18790
- `;
18791
- }
18792
- case "cli":
18793
- return `TEST STRATEGY: cli
18794
- Use Bun.spawn to run the binary. Read stdout and assert on the text output.
18795
-
18796
- `;
18797
- case "e2e":
18798
- return `TEST STRATEGY: e2e
18799
- Use fetch() against http://localhost to call the running service. Assert on response body using response.text() or response.json().
18800
-
18801
- `;
18802
- case "snapshot":
18803
- return `TEST STRATEGY: snapshot
18804
- Render the component and use toMatchSnapshot() to capture and compare snapshots.
18805
-
18806
- `;
18807
- default:
18808
- return "";
18809
- }
18810
- }
18811
18795
  function parseAcceptanceCriteria(specContent) {
18812
18796
  const criteria = [];
18813
18797
  const lines = specContent.split(`
@@ -18831,46 +18815,38 @@ function parseAcceptanceCriteria(specContent) {
18831
18815
  function buildAcceptanceTestPrompt(criteria, featureName, codebaseContext) {
18832
18816
  const criteriaList = criteria.map((ac) => `${ac.id}: ${ac.text}`).join(`
18833
18817
  `);
18834
- return `You are a test engineer. Generate acceptance tests for the "${featureName}" feature based on the acceptance criteria below.
18818
+ return `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${featureName}" feature.
18835
18819
 
18836
- CODEBASE CONTEXT:
18837
- ${codebaseContext}
18820
+ ## Step 1: Understand and Classify the Acceptance Criteria
18821
+
18822
+ Read each AC below and classify its verification type:
18823
+ - **file-check**: Verify by reading source files (e.g. "no @nestjs/jwt imports", "file exists", "module registered", "uses registerAs pattern")
18824
+ - **runtime-check**: Load and invoke code directly, assert on return values or behavior
18825
+ - **integration-check**: Requires a running service (e.g. HTTP endpoint returns 200, 11th request returns 429, database query succeeds)
18838
18826
 
18839
18827
  ACCEPTANCE CRITERIA:
18840
18828
  ${criteriaList}
18841
18829
 
18842
- Generate a complete acceptance.test.ts file using bun:test framework. Follow these rules:
18843
-
18844
- 1. **One test per AC**: Each acceptance criterion maps to exactly one test
18845
- 2. **Test observable behavior only**: No implementation details, only user-facing behavior
18846
- 3. **Independent tests**: No shared state between tests
18847
- 4. **Real-implementation**: Tests should use real implementations without mocking (test observable behavior, not internal units)
18848
- 5. **Clear test names**: Use format "AC-N: <description>" for test names
18849
- 6. **Async where needed**: Use async/await for operations that may be asynchronous
18830
+ ## Step 2: Explore the Project
18850
18831
 
18851
- Use this structure:
18832
+ Before writing any tests, examine the project to understand:
18833
+ 1. **Language and test framework** \u2014 check dependency manifests (package.json, go.mod, Gemfile, pyproject.toml, Cargo.toml, build.gradle, etc.) to identify the language and test runner
18834
+ 2. **Existing test patterns** \u2014 read 1-2 existing test files to understand import style, describe/test/it conventions, and available helpers
18835
+ 3. **Project structure** \u2014 identify relevant source directories to determine correct import or load paths
18852
18836
 
18853
- \`\`\`typescript
18854
- import { describe, test, expect } from "bun:test";
18855
-
18856
- describe("${featureName} - Acceptance Tests", () => {
18857
- test("AC-1: <description>", async () => {
18858
- // Test implementation
18859
- });
18860
18837
 
18861
- test("AC-2: <description>", async () => {
18862
- // Test implementation
18863
- });
18864
- });
18865
- \`\`\`
18838
+ ## Step 3: Generate the Acceptance Test File
18866
18839
 
18867
- **Important**:
18868
- - Import the feature code being tested
18869
- - Set up any necessary test fixtures
18870
- - Use expect() assertions to verify behavior
18871
- - Clean up resources if needed (close connections, delete temp files)
18840
+ Write the complete acceptance test file using the framework identified in Step 2.
18872
18841
 
18873
- Respond with ONLY the TypeScript test code (no markdown code fences, no explanation).`;
18842
+ Rules:
18843
+ - **One test per AC**, named exactly "AC-N: <description>"
18844
+ - **file-check ACs** \u2192 read source files using the language's standard file I/O, assert with string or regex checks. Do not start the application.
18845
+ - **runtime-check ACs** \u2192 load or import the module directly and invoke it, assert on the return value or observable side effects
18846
+ - **integration-check ACs** \u2192 use the language's HTTP client or existing test helpers; add a clear setup block (beforeAll/setup/TestMain/etc.) explaining what must be running
18847
+ - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18848
+ - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18849
+ - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration`;
18874
18850
  }
18875
18851
  async function generateAcceptanceTests(adapter, options) {
18876
18852
  const logger = getLogger();
@@ -18887,7 +18863,9 @@ async function generateAcceptanceTests(adapter, options) {
18887
18863
  try {
18888
18864
  const output = await adapter.complete(prompt, {
18889
18865
  model: options.modelDef.model,
18890
- config: options.config
18866
+ config: options.config,
18867
+ timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
18868
+ workdir: options.workdir
18891
18869
  });
18892
18870
  const testCode = extractTestCode(output);
18893
18871
  if (!testCode) {
@@ -22351,7 +22329,7 @@ var package_default;
22351
22329
  var init_package = __esm(() => {
22352
22330
  package_default = {
22353
22331
  name: "@nathapp/nax",
22354
- version: "0.50.2",
22332
+ version: "0.50.3",
22355
22333
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22356
22334
  type: "module",
22357
22335
  bin: {
@@ -22425,8 +22403,8 @@ var init_version = __esm(() => {
22425
22403
  NAX_VERSION = package_default.version;
22426
22404
  NAX_COMMIT = (() => {
22427
22405
  try {
22428
- if (/^[0-9a-f]{6,10}$/.test("c3a5edb"))
22429
- return "c3a5edb";
22406
+ if (/^[0-9a-f]{6,10}$/.test("684b48b"))
22407
+ return "684b48b";
22430
22408
  } catch {}
22431
22409
  try {
22432
22410
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24180,7 +24158,105 @@ ${stderr}`;
24180
24158
  };
24181
24159
  });
24182
24160
 
24161
+ // src/agents/shared/validation.ts
24162
+ function validateAgentForTier(agent, tier) {
24163
+ return agent.capabilities.supportedTiers.includes(tier);
24164
+ }
24165
+ function validateAgentFeature(agent, feature) {
24166
+ return agent.capabilities.features.has(feature);
24167
+ }
24168
+ function describeAgentCapabilities(agent) {
24169
+ const tiers = agent.capabilities.supportedTiers.join(",");
24170
+ const features = Array.from(agent.capabilities.features).join(",");
24171
+ const maxTokens = agent.capabilities.maxContextTokens;
24172
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
24173
+ }
24174
+
24175
+ // src/agents/shared/version-detection.ts
24176
+ async function getAgentVersion(binaryName) {
24177
+ try {
24178
+ const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
24179
+ stdout: "pipe",
24180
+ stderr: "pipe"
24181
+ });
24182
+ const exitCode = await proc.exited;
24183
+ if (exitCode !== 0) {
24184
+ return null;
24185
+ }
24186
+ const stdout = await new Response(proc.stdout).text();
24187
+ const versionLine = stdout.trim().split(`
24188
+ `)[0];
24189
+ const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
24190
+ if (versionMatch) {
24191
+ return versionMatch[0];
24192
+ }
24193
+ return versionLine || null;
24194
+ } catch {
24195
+ return null;
24196
+ }
24197
+ }
24198
+ async function getAgentVersions() {
24199
+ const agents = await getInstalledAgents();
24200
+ const agentsByName = new Map(agents.map((a) => [a.name, a]));
24201
+ const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
24202
+ const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
24203
+ const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
24204
+ return {
24205
+ name: agent.name,
24206
+ displayName: agent.displayName,
24207
+ version: version2,
24208
+ installed: agentsByName.has(agent.name)
24209
+ };
24210
+ }));
24211
+ return versions2;
24212
+ }
24213
+ var _versionDetectionDeps;
24214
+ var init_version_detection = __esm(() => {
24215
+ init_registry();
24216
+ _versionDetectionDeps = {
24217
+ spawn(cmd, opts) {
24218
+ return Bun.spawn(cmd, opts);
24219
+ }
24220
+ };
24221
+ });
24222
+
24223
+ // src/agents/index.ts
24224
+ var exports_agents = {};
24225
+ __export(exports_agents, {
24226
+ validateAgentForTier: () => validateAgentForTier,
24227
+ validateAgentFeature: () => validateAgentFeature,
24228
+ parseTokenUsage: () => parseTokenUsage,
24229
+ getInstalledAgents: () => getInstalledAgents,
24230
+ getAllAgentNames: () => getAllAgentNames,
24231
+ getAgentVersions: () => getAgentVersions,
24232
+ getAgentVersion: () => getAgentVersion,
24233
+ getAgent: () => getAgent,
24234
+ formatCostWithConfidence: () => formatCostWithConfidence,
24235
+ estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
24236
+ estimateCostFromOutput: () => estimateCostFromOutput,
24237
+ estimateCostByDuration: () => estimateCostByDuration,
24238
+ estimateCost: () => estimateCost,
24239
+ describeAgentCapabilities: () => describeAgentCapabilities,
24240
+ checkAgentHealth: () => checkAgentHealth,
24241
+ MODEL_PRICING: () => MODEL_PRICING,
24242
+ CompleteError: () => CompleteError,
24243
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
24244
+ COST_RATES: () => COST_RATES
24245
+ });
24246
+ var init_agents = __esm(() => {
24247
+ init_types2();
24248
+ init_claude();
24249
+ init_registry();
24250
+ init_cost();
24251
+ init_version_detection();
24252
+ });
24253
+
24183
24254
  // src/pipeline/stages/acceptance-setup.ts
24255
+ var exports_acceptance_setup = {};
24256
+ __export(exports_acceptance_setup, {
24257
+ acceptanceSetupStage: () => acceptanceSetupStage,
24258
+ _acceptanceSetupDeps: () => _acceptanceSetupDeps
24259
+ });
24184
24260
  import path5 from "path";
24185
24261
  var _acceptanceSetupDeps, acceptanceSetupStage;
24186
24262
  var init_acceptance_setup = __esm(() => {
@@ -24232,6 +24308,8 @@ ${stderr}` };
24232
24308
  if (!fileExists) {
24233
24309
  const allCriteria = ctx.prd.userStories.flatMap((s) => s.acceptanceCriteria);
24234
24310
  totalCriteria = allCriteria.length;
24311
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
24312
+ const agent = (ctx.agentGetFn ?? getAgent2)(ctx.config.autoMode.defaultAgent);
24235
24313
  let refinedCriteria;
24236
24314
  if (ctx.config.acceptance.refinement) {
24237
24315
  refinedCriteria = await _acceptanceSetupDeps.refine(allCriteria, {
@@ -24259,7 +24337,8 @@ ${stderr}` };
24259
24337
  modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
24260
24338
  config: ctx.config,
24261
24339
  testStrategy: ctx.config.acceptance.testStrategy,
24262
- testFramework: ctx.config.acceptance.testFramework
24340
+ testFramework: ctx.config.acceptance.testFramework,
24341
+ adapter: agent ?? undefined
24263
24342
  });
24264
24343
  await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
24265
24344
  }
@@ -24281,99 +24360,6 @@ ${stderr}` };
24281
24360
  };
24282
24361
  });
24283
24362
 
24284
- // src/agents/shared/validation.ts
24285
- function validateAgentForTier(agent, tier) {
24286
- return agent.capabilities.supportedTiers.includes(tier);
24287
- }
24288
- function validateAgentFeature(agent, feature) {
24289
- return agent.capabilities.features.has(feature);
24290
- }
24291
- function describeAgentCapabilities(agent) {
24292
- const tiers = agent.capabilities.supportedTiers.join(",");
24293
- const features = Array.from(agent.capabilities.features).join(",");
24294
- const maxTokens = agent.capabilities.maxContextTokens;
24295
- return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
24296
- }
24297
-
24298
- // src/agents/shared/version-detection.ts
24299
- async function getAgentVersion(binaryName) {
24300
- try {
24301
- const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
24302
- stdout: "pipe",
24303
- stderr: "pipe"
24304
- });
24305
- const exitCode = await proc.exited;
24306
- if (exitCode !== 0) {
24307
- return null;
24308
- }
24309
- const stdout = await new Response(proc.stdout).text();
24310
- const versionLine = stdout.trim().split(`
24311
- `)[0];
24312
- const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
24313
- if (versionMatch) {
24314
- return versionMatch[0];
24315
- }
24316
- return versionLine || null;
24317
- } catch {
24318
- return null;
24319
- }
24320
- }
24321
- async function getAgentVersions() {
24322
- const agents = await getInstalledAgents();
24323
- const agentsByName = new Map(agents.map((a) => [a.name, a]));
24324
- const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
24325
- const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
24326
- const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
24327
- return {
24328
- name: agent.name,
24329
- displayName: agent.displayName,
24330
- version: version2,
24331
- installed: agentsByName.has(agent.name)
24332
- };
24333
- }));
24334
- return versions2;
24335
- }
24336
- var _versionDetectionDeps;
24337
- var init_version_detection = __esm(() => {
24338
- init_registry();
24339
- _versionDetectionDeps = {
24340
- spawn(cmd, opts) {
24341
- return Bun.spawn(cmd, opts);
24342
- }
24343
- };
24344
- });
24345
-
24346
- // src/agents/index.ts
24347
- var exports_agents = {};
24348
- __export(exports_agents, {
24349
- validateAgentForTier: () => validateAgentForTier,
24350
- validateAgentFeature: () => validateAgentFeature,
24351
- parseTokenUsage: () => parseTokenUsage,
24352
- getInstalledAgents: () => getInstalledAgents,
24353
- getAllAgentNames: () => getAllAgentNames,
24354
- getAgentVersions: () => getAgentVersions,
24355
- getAgentVersion: () => getAgentVersion,
24356
- getAgent: () => getAgent,
24357
- formatCostWithConfidence: () => formatCostWithConfidence,
24358
- estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
24359
- estimateCostFromOutput: () => estimateCostFromOutput,
24360
- estimateCostByDuration: () => estimateCostByDuration,
24361
- estimateCost: () => estimateCost,
24362
- describeAgentCapabilities: () => describeAgentCapabilities,
24363
- checkAgentHealth: () => checkAgentHealth,
24364
- MODEL_PRICING: () => MODEL_PRICING,
24365
- CompleteError: () => CompleteError,
24366
- ClaudeCodeAdapter: () => ClaudeCodeAdapter,
24367
- COST_RATES: () => COST_RATES
24368
- });
24369
- var init_agents = __esm(() => {
24370
- init_types2();
24371
- init_claude();
24372
- init_registry();
24373
- init_cost();
24374
- init_version_detection();
24375
- });
24376
-
24377
24363
  // src/pipeline/event-bus.ts
24378
24364
  class PipelineEventBus {
24379
24365
  subscribers = new Map;
@@ -32201,9 +32187,13 @@ var init_crash_recovery = __esm(() => {
32201
32187
  // src/execution/lifecycle/acceptance-loop.ts
32202
32188
  var exports_acceptance_loop = {};
32203
32189
  __export(exports_acceptance_loop, {
32204
- runAcceptanceLoop: () => runAcceptanceLoop
32190
+ runAcceptanceLoop: () => runAcceptanceLoop,
32191
+ isStubTestFile: () => isStubTestFile
32205
32192
  });
32206
32193
  import path14 from "path";
32194
+ function isStubTestFile(content) {
32195
+ return /expect\s*\(\s*true\s*\)\s*\.\s*toBe\s*\(\s*(?:false|true)\s*\)/.test(content);
32196
+ }
32207
32197
  async function loadSpecContent(featureDir) {
32208
32198
  if (!featureDir)
32209
32199
  return "";
@@ -32337,6 +32327,25 @@ async function runAcceptanceLoop(ctx) {
32337
32327
  }), ctx.workdir);
32338
32328
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32339
32329
  }
32330
+ if (ctx.featureDir) {
32331
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
32332
+ const testFile = Bun.file(testPath);
32333
+ if (await testFile.exists()) {
32334
+ const testContent = await testFile.text();
32335
+ if (isStubTestFile(testContent)) {
32336
+ logger?.warn("acceptance", "Stub tests detected \u2014 re-generating acceptance tests");
32337
+ const { unlink: unlink3 } = await import("fs/promises");
32338
+ await unlink3(testPath);
32339
+ const { acceptanceSetupStage: acceptanceSetupStage2 } = await Promise.resolve().then(() => (init_acceptance_setup(), exports_acceptance_setup));
32340
+ await acceptanceSetupStage2.execute(acceptanceContext);
32341
+ const newContent = await Bun.file(testPath).text();
32342
+ if (isStubTestFile(newContent)) {
32343
+ logger?.error("acceptance", "Acceptance test generation failed after retry \u2014 manual implementation required");
32344
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32345
+ }
32346
+ }
32347
+ }
32348
+ }
32340
32349
  logger?.info("acceptance", "Generating fix stories...");
32341
32350
  const fixStories = await generateAndAddFixStories(ctx, failures, prd);
32342
32351
  if (!fixStories) {
@@ -69630,6 +69639,7 @@ var FIELD_DESCRIPTIONS = {
69630
69639
  "acceptance.maxRetries": "Max retry loops for fix stories",
69631
69640
  "acceptance.generateTests": "Generate acceptance tests during analyze",
69632
69641
  "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
69642
+ "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
69633
69643
  context: "Context injection configuration",
69634
69644
  "context.fileInjection": "Mode: 'disabled' (default, MCP-aware agents pull context on-demand) | 'keyword' (legacy git-grep injection for non-MCP agents). Set context.fileInjection in config.",
69635
69645
  "context.testCoverage": "Test coverage context settings",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.50.2",
3
+ "version": "0.50.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,35 +82,53 @@ 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);
85
+ const frameworkOverrideLine = options.testFramework
86
+ ? `\n[FRAMEWORK OVERRIDE: Use ${options.testFramework} as the test framework regardless of what you detect.]`
87
+ : "";
86
88
 
87
- const prompt = `You are a test engineer. Generate acceptance tests for the "${options.featureName}" feature based on the refined acceptance criteria below.
89
+ const basePrompt = `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${options.featureName}" feature.
88
90
 
89
- CODEBASE CONTEXT:
90
- ${options.codebaseContext}
91
+ ## Step 1: Understand and Classify the Acceptance Criteria
91
92
 
92
- ACCEPTANCE CRITERIA (refined):
93
+ Read each AC below and classify its verification type:
94
+ - **file-check**: Verify by reading source files (e.g. "no @nestjs/jwt imports", "file exists", "module registered", "uses registerAs pattern")
95
+ - **runtime-check**: Load and invoke code directly, assert on return values or behavior
96
+ - **integration-check**: Requires a running service (e.g. HTTP endpoint returns 200, 11th request returns 429, database query succeeds)
97
+
98
+ ACCEPTANCE CRITERIA:
93
99
  ${criteriaList}
94
100
 
95
- ${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
101
+ ## Step 2: Explore the Project
96
102
 
97
- Structure example (do NOT wrap in markdown fences — output raw TypeScript only):
103
+ Before writing any tests, examine the project to understand:
104
+ 1. **Language and test framework** — check dependency manifests (package.json, go.mod, Gemfile, pyproject.toml, Cargo.toml, build.gradle, etc.) to identify the language and test runner
105
+ 2. **Existing test patterns** — read 1-2 existing test files to understand import style, describe/test/it conventions, and available helpers
106
+ 3. **Project structure** — identify relevant source directories to determine correct import or load paths
98
107
 
99
- import { describe, test, expect } from "bun:test";
108
+ ${frameworkOverrideLine}
100
109
 
101
- describe("${options.featureName} - Acceptance Tests", () => {
102
- test("AC-1: <description>", async () => {
103
- // Test implementation
104
- });
105
- });
110
+ ## Step 3: Generate the Acceptance Test File
111
+
112
+ Write the complete acceptance test file using the framework identified in Step 2.
106
113
 
107
- IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\`\`typescript or \`\`\`). Start directly with the import statement.`;
114
+ Rules:
115
+ - **One test per AC**, named exactly "AC-N: <description>"
116
+ - **file-check ACs** → read source files using the language's standard file I/O, assert with string or regex checks. Do not start the application.
117
+ - **runtime-check ACs** → load or import the module directly and invoke it, assert on the return value or observable side effects
118
+ - **integration-check ACs** → use the language's HTTP client or existing test helpers; add a clear setup block (beforeAll/setup/TestMain/etc.) explaining what must be running
119
+ - **NEVER use placeholder assertions** — no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
120
+ - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
121
+ - Output raw code only — no markdown fences, start directly with the language's import or package declaration`;
122
+
123
+ const prompt = basePrompt;
108
124
 
109
125
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
110
126
 
111
- const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
127
+ const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
112
128
  model: options.modelDef.model,
113
129
  config: options.config,
130
+ timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
131
+ workdir: options.workdir,
114
132
  });
115
133
  const testCode = extractTestCode(rawOutput);
116
134
 
@@ -143,26 +161,6 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
143
161
  return { testCode, criteria };
144
162
  }
145
163
 
146
- function buildStrategyInstructions(strategy?: string, framework?: string): string {
147
- switch (strategy) {
148
- case "component": {
149
- const fw = framework ?? "ink-testing-library";
150
- if (fw === "react") {
151
- 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";
152
- }
153
- 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";
154
- }
155
- case "cli":
156
- return "TEST STRATEGY: cli\nUse Bun.spawn to run the binary. Read stdout and assert on the text output.\n\n";
157
- case "e2e":
158
- 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";
159
- case "snapshot":
160
- return "TEST STRATEGY: snapshot\nRender the component and use toMatchSnapshot() to capture and compare snapshots.\n\n";
161
- default:
162
- return "";
163
- }
164
- }
165
-
166
164
  export function parseAcceptanceCriteria(specContent: string): AcceptanceCriterion[] {
167
165
  const criteria: AcceptanceCriterion[] = [];
168
166
  const lines = specContent.split("\n");
@@ -218,46 +216,38 @@ export function buildAcceptanceTestPrompt(
218
216
  ): string {
219
217
  const criteriaList = criteria.map((ac) => `${ac.id}: ${ac.text}`).join("\n");
220
218
 
221
- return `You are a test engineer. Generate acceptance tests for the "${featureName}" feature based on the acceptance criteria below.
219
+ return `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${featureName}" feature.
220
+
221
+ ## Step 1: Understand and Classify the Acceptance Criteria
222
222
 
223
- CODEBASE CONTEXT:
224
- ${codebaseContext}
223
+ Read each AC below and classify its verification type:
224
+ - **file-check**: Verify by reading source files (e.g. "no @nestjs/jwt imports", "file exists", "module registered", "uses registerAs pattern")
225
+ - **runtime-check**: Load and invoke code directly, assert on return values or behavior
226
+ - **integration-check**: Requires a running service (e.g. HTTP endpoint returns 200, 11th request returns 429, database query succeeds)
225
227
 
226
228
  ACCEPTANCE CRITERIA:
227
229
  ${criteriaList}
228
230
 
229
- Generate a complete acceptance.test.ts file using bun:test framework. Follow these rules:
230
-
231
- 1. **One test per AC**: Each acceptance criterion maps to exactly one test
232
- 2. **Test observable behavior only**: No implementation details, only user-facing behavior
233
- 3. **Independent tests**: No shared state between tests
234
- 4. **Real-implementation**: Tests should use real implementations without mocking (test observable behavior, not internal units)
235
- 5. **Clear test names**: Use format "AC-N: <description>" for test names
236
- 6. **Async where needed**: Use async/await for operations that may be asynchronous
237
-
238
- Use this structure:
231
+ ## Step 2: Explore the Project
239
232
 
240
- \`\`\`typescript
241
- import { describe, test, expect } from "bun:test";
233
+ Before writing any tests, examine the project to understand:
234
+ 1. **Language and test framework** — check dependency manifests (package.json, go.mod, Gemfile, pyproject.toml, Cargo.toml, build.gradle, etc.) to identify the language and test runner
235
+ 2. **Existing test patterns** — read 1-2 existing test files to understand import style, describe/test/it conventions, and available helpers
236
+ 3. **Project structure** — identify relevant source directories to determine correct import or load paths
242
237
 
243
- describe("${featureName} - Acceptance Tests", () => {
244
- test("AC-1: <description>", async () => {
245
- // Test implementation
246
- });
247
238
 
248
- test("AC-2: <description>", async () => {
249
- // Test implementation
250
- });
251
- });
252
- \`\`\`
239
+ ## Step 3: Generate the Acceptance Test File
253
240
 
254
- **Important**:
255
- - Import the feature code being tested
256
- - Set up any necessary test fixtures
257
- - Use expect() assertions to verify behavior
258
- - Clean up resources if needed (close connections, delete temp files)
241
+ Write the complete acceptance test file using the framework identified in Step 2.
259
242
 
260
- Respond with ONLY the TypeScript test code (no markdown code fences, no explanation).`;
243
+ Rules:
244
+ - **One test per AC**, named exactly "AC-N: <description>"
245
+ - **file-check ACs** → read source files using the language's standard file I/O, assert with string or regex checks. Do not start the application.
246
+ - **runtime-check ACs** → load or import the module directly and invoke it, assert on the return value or observable side effects
247
+ - **integration-check ACs** → use the language's HTTP client or existing test helpers; add a clear setup block (beforeAll/setup/TestMain/etc.) explaining what must be running
248
+ - **NEVER use placeholder assertions** — no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
249
+ - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
250
+ - Output raw code only — no markdown fences, start directly with the language's import or package declaration`;
261
251
  }
262
252
 
263
253
  /**
@@ -313,6 +303,8 @@ export async function generateAcceptanceTests(
313
303
  const output = await adapter.complete(prompt, {
314
304
  model: options.modelDef.model,
315
305
  config: options.config,
306
+ timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
307
+ workdir: options.workdir,
316
308
  });
317
309
 
318
310
  // Extract test code from output
@@ -4,6 +4,7 @@
4
4
  * Types for generating acceptance tests from spec.md acceptance criteria.
5
5
  */
6
6
 
7
+ import type { AgentAdapter } from "../agents/types";
7
8
  import type { AcceptanceTestStrategy, ModelDef, ModelTier, NaxConfig } from "../config/schema";
8
9
 
9
10
  /**
@@ -94,6 +95,8 @@ export interface GenerateFromPRDOptions {
94
95
  testStrategy?: AcceptanceTestStrategy;
95
96
  /** Test framework for component/snapshot strategies (e.g. 'ink-testing-library', 'react') */
96
97
  testFramework?: string;
98
+ /** Agent adapter to use for test generation — overrides _generatorPRDDeps.adapter */
99
+ adapter?: AgentAdapter;
97
100
  }
98
101
 
99
102
  export interface GenerateAcceptanceTestsOptions {
@@ -141,6 +141,7 @@ export const FIELD_DESCRIPTIONS: Record<string, string> = {
141
141
  "acceptance.maxRetries": "Max retry loops for fix stories",
142
142
  "acceptance.generateTests": "Generate acceptance tests during analyze",
143
143
  "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
144
+ "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
144
145
 
145
146
  // Context
146
147
  context: "Context injection configuration",
@@ -168,6 +168,7 @@ export const DEFAULT_CONFIG: NaxConfig = {
168
168
  model: "fast" as const,
169
169
  refinement: true,
170
170
  redGate: true,
171
+ timeoutMs: 1800000,
171
172
  },
172
173
  context: {
173
174
  fileInjection: "disabled",
@@ -262,6 +262,8 @@ export interface AcceptanceConfig {
262
262
  testStrategy?: AcceptanceTestStrategy;
263
263
  /** Test framework for acceptance tests (default: auto-detect) */
264
264
  testFramework?: string;
265
+ /** Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min) */
266
+ timeoutMs: number;
265
267
  }
266
268
 
267
269
  /** Optimizer config (v0.10) */
@@ -257,6 +257,7 @@ export const AcceptanceConfigSchema = z.object({
257
257
  redGate: z.boolean().default(true),
258
258
  testStrategy: z.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
259
259
  testFramework: z.string().min(1, "acceptance.testFramework must be non-empty").optional(),
260
+ timeoutMs: z.number().int().min(30000).max(3600000).default(1800000),
260
261
  });
261
262
 
262
263
  const TestCoverageConfigSchema = z.object({
@@ -55,6 +55,11 @@ export interface AcceptanceLoopResult {
55
55
  prdDirty: boolean;
56
56
  }
57
57
 
58
+ export function isStubTestFile(content: string): boolean {
59
+ // Detect skeleton stubs: expect(true).toBe(false) or expect(true).toBe(true) in test bodies
60
+ return /expect\s*\(\s*true\s*\)\s*\.\s*toBe\s*\(\s*(?:false|true)\s*\)/.test(content);
61
+ }
62
+
58
63
  /** Load spec.md content for AC text */
59
64
  async function loadSpecContent(featureDir?: string): Promise<string> {
60
65
  if (!featureDir) return "";
@@ -243,6 +248,30 @@ export async function runAcceptanceLoop(ctx: AcceptanceLoopContext): Promise<Acc
243
248
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
244
249
  }
245
250
 
251
+ // Check for stub test file before generating fix stories
252
+ if (ctx.featureDir) {
253
+ const testPath = path.join(ctx.featureDir, "acceptance.test.ts");
254
+ const testFile = Bun.file(testPath);
255
+ if (await testFile.exists()) {
256
+ const testContent = await testFile.text();
257
+ if (isStubTestFile(testContent)) {
258
+ logger?.warn("acceptance", "Stub tests detected — re-generating acceptance tests");
259
+ const { unlink } = await import("node:fs/promises");
260
+ await unlink(testPath);
261
+ const { acceptanceSetupStage } = await import("../../pipeline/stages/acceptance-setup");
262
+ await acceptanceSetupStage.execute(acceptanceContext);
263
+ const newContent = await Bun.file(testPath).text();
264
+ if (isStubTestFile(newContent)) {
265
+ logger?.error(
266
+ "acceptance",
267
+ "Acceptance test generation failed after retry — manual implementation required",
268
+ );
269
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
270
+ }
271
+ }
272
+ }
273
+ }
274
+
246
275
  // Generate and add fix stories
247
276
  logger?.info("acceptance", "Generating fix stories...");
248
277
  const fixStories = await generateAndAddFixStories(ctx, failures, prd);
@@ -82,6 +82,9 @@ export const acceptanceSetupStage: PipelineStage = {
82
82
  const allCriteria: string[] = ctx.prd.userStories.flatMap((s) => s.acceptanceCriteria);
83
83
  totalCriteria = allCriteria.length;
84
84
 
85
+ const { getAgent } = await import("../../agents");
86
+ const agent = (ctx.agentGetFn ?? getAgent)(ctx.config.autoMode.defaultAgent);
87
+
85
88
  let refinedCriteria: RefinedCriterion[];
86
89
 
87
90
  if (ctx.config.acceptance.refinement) {
@@ -113,6 +116,7 @@ export const acceptanceSetupStage: PipelineStage = {
113
116
  config: ctx.config,
114
117
  testStrategy: ctx.config.acceptance.testStrategy,
115
118
  testFramework: ctx.config.acceptance.testFramework,
119
+ adapter: agent ?? undefined,
116
120
  });
117
121
 
118
122
  await _acceptanceSetupDeps.writeFile(testPath, result.testCode);