@nathapp/nax 0.20.0 → 0.22.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 (233) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.mcp.json +8 -0
  3. package/docs/20260304-review-nax.md +492 -0
  4. package/docs/ROADMAP.md +65 -18
  5. package/docs/adr/ADR-005-implementation-plan.md +655 -0
  6. package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
  7. package/docs/specs/bug-039-orphan-processes.md +131 -0
  8. package/docs/specs/bug-040-review-rectification.md +82 -0
  9. package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
  10. package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
  11. package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
  12. package/docs/specs/feat-011-file-context-strategy.md +73 -0
  13. package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
  14. package/docs/specs/feat-013-test-after-review.md +89 -0
  15. package/docs/specs/feat-014-heartbeat-observability.md +127 -0
  16. package/memory/topic/feat-010-baseref.md +28 -0
  17. package/memory/topic/feat-013-test-after-deprecation.md +22 -0
  18. package/nax/config.json +7 -4
  19. package/nax/features/bug-039-medium/prd.json +45 -0
  20. package/package.json +2 -2
  21. package/src/agents/claude.ts +109 -15
  22. package/src/config/types.ts +11 -0
  23. package/src/context/builder.ts +9 -1
  24. package/src/execution/dry-run.ts +81 -0
  25. package/src/execution/escalation/tier-outcome.ts +29 -44
  26. package/src/execution/executor-types.ts +65 -0
  27. package/src/execution/index.ts +0 -17
  28. package/src/execution/iteration-runner.ts +132 -0
  29. package/src/execution/lifecycle/index.ts +0 -1
  30. package/src/execution/lifecycle/run-regression.ts +5 -5
  31. package/src/execution/pipeline-result-handler.ts +51 -254
  32. package/src/execution/sequential-executor.ts +72 -315
  33. package/src/execution/story-selector.ts +75 -0
  34. package/src/pipeline/event-bus.ts +276 -0
  35. package/src/pipeline/runner.ts +51 -77
  36. package/src/pipeline/stages/autofix.ts +133 -0
  37. package/src/pipeline/stages/completion.ts +22 -30
  38. package/src/pipeline/stages/index.ts +30 -13
  39. package/src/pipeline/stages/rectify.ts +93 -0
  40. package/src/pipeline/stages/regression.ts +88 -0
  41. package/src/pipeline/stages/review.ts +19 -153
  42. package/src/pipeline/stages/verify.ts +19 -3
  43. package/src/pipeline/subscribers/hooks.ts +133 -0
  44. package/src/pipeline/subscribers/interaction.ts +68 -0
  45. package/src/pipeline/subscribers/reporters.ts +174 -0
  46. package/src/pipeline/types.ts +12 -1
  47. package/src/review/orchestrator.ts +105 -0
  48. package/src/review/runner.ts +39 -4
  49. package/src/routing/router.ts +3 -3
  50. package/src/routing/strategies/keyword.ts +5 -2
  51. package/src/routing/strategies/llm.ts +27 -1
  52. package/src/tdd/prompts.ts +1 -1
  53. package/src/utils/git.ts +49 -25
  54. package/src/verification/executor.ts +8 -2
  55. package/src/verification/index.ts +1 -1
  56. package/src/verification/orchestrator-types.ts +145 -0
  57. package/src/verification/orchestrator.ts +76 -0
  58. package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
  59. package/src/verification/{gate.ts → runners.ts} +17 -105
  60. package/src/verification/smart-runner.ts +6 -10
  61. package/src/verification/strategies/acceptance.ts +133 -0
  62. package/src/verification/strategies/regression.ts +90 -0
  63. package/src/verification/strategies/scoped.ts +123 -0
  64. package/test/COVERAGE-GAPS.md +333 -0
  65. package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
  66. package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
  67. package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
  68. package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
  69. package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
  70. package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
  71. package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
  72. package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
  73. package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
  74. package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
  75. package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
  76. package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
  77. package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
  78. package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
  79. package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
  80. package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
  81. package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
  82. package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
  83. package/test/integration/config/merger.test.ts +1 -0
  84. package/test/integration/config/paths.test.ts +1 -0
  85. package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
  86. package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
  87. package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
  88. package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
  89. package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
  90. package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
  91. package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
  92. package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
  93. package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
  94. package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
  95. package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
  96. package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
  97. package/test/integration/execution/runner-batching.test.ts +682 -0
  98. package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
  99. package/test/integration/execution/runner-escalation.test.ts +561 -0
  100. package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
  101. package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
  102. package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
  103. package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
  104. package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
  105. package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
  106. package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
  107. package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
  108. package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
  109. package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
  110. package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
  111. package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
  112. package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
  113. package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
  114. package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
  115. package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
  116. package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
  117. package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
  118. package/test/integration/plugins/config-integration.test.ts +1 -0
  119. package/test/integration/plugins/config-resolution.test.ts +1 -0
  120. package/test/integration/plugins/loader.test.ts +1 -0
  121. package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
  122. package/test/integration/plugins/validator.test.ts +1 -0
  123. package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
  124. package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
  125. package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
  126. package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
  127. package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
  128. package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +10 -404
  129. package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
  130. package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
  131. package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
  132. package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
  133. package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
  134. package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
  135. package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
  136. package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
  137. package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
  138. package/test/integration/tmp/headless-test/test.jsonl +30 -0
  139. package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
  140. package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
  141. package/test/unit/acceptance.test.ts +1 -0
  142. package/test/unit/agent-stderr-capture.test.ts +1 -0
  143. package/test/unit/agents/claude.test.ts +107 -0
  144. package/test/unit/analyze-classifier.test.ts +1 -0
  145. package/test/unit/auto-detect.test.ts +1 -0
  146. package/test/unit/cli-status.test.ts +1 -0
  147. package/test/unit/commands/common.test.ts +1 -0
  148. package/test/unit/commands/logs.test.ts +1 -0
  149. package/test/unit/commands/unlock.test.ts +1 -0
  150. package/test/unit/config/defaults.test.ts +1 -0
  151. package/test/unit/config/regression-gate-schema.test.ts +1 -0
  152. package/test/unit/config/smart-runner-flag.test.ts +1 -0
  153. package/test/unit/constitution-generators.test.ts +1 -0
  154. package/test/unit/constitution.test.ts +1 -0
  155. package/test/unit/context/context-autodetect.test.ts +297 -0
  156. package/test/unit/context/context-build.test.ts +575 -0
  157. package/test/unit/context/context-coverage.test.ts +236 -0
  158. package/test/unit/context/context-error.test.ts +93 -0
  159. package/test/unit/context/context-estimate-tokens.test.ts +201 -0
  160. package/test/unit/context/context-format.test.ts +302 -0
  161. package/test/unit/context/context-isolation.test.ts +267 -0
  162. package/test/unit/context/context-sort.test.ts +93 -0
  163. package/test/unit/context/context-story.test.ts +108 -0
  164. package/test/{context → unit/context}/prior-failures.test.ts +5 -4
  165. package/test/unit/context.test.ts +7 -3
  166. package/test/unit/crash-recovery.test.ts +1 -0
  167. package/test/unit/escalation.test.ts +1 -0
  168. package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
  169. package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
  170. package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
  171. package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
  172. package/test/unit/execution-logging-stderr.test.ts +1 -0
  173. package/test/unit/execution-stage.test.ts +1 -0
  174. package/test/unit/fix-generator.test.ts +1 -0
  175. package/test/unit/greenfield.test.ts +1 -0
  176. package/test/unit/interaction/human-review-trigger.test.ts +1 -0
  177. package/test/unit/interaction-network-failures.test.ts +1 -0
  178. package/test/unit/interaction-plugins.test.ts +1 -0
  179. package/test/unit/logging/formatter.test.ts +1 -0
  180. package/test/unit/merge.test.ts +1 -0
  181. package/test/unit/pipeline/event-bus.test.ts +105 -0
  182. package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
  183. package/test/unit/pipeline/runner-retry.test.ts +89 -0
  184. package/test/unit/pipeline/stages/autofix.test.ts +97 -0
  185. package/test/unit/pipeline/stages/rectify.test.ts +101 -0
  186. package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
  187. package/test/unit/pipeline/stages/verify.test.ts +1 -0
  188. package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
  189. package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
  190. package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
  191. package/test/unit/pipeline/verify-smart-runner.test.ts +2 -1
  192. package/test/unit/prd-auto-default.test.ts +3 -2
  193. package/test/unit/prd-failure-category.test.ts +1 -0
  194. package/test/unit/prd-get-next-story.test.ts +1 -0
  195. package/test/unit/precheck-checks.test.ts +1 -0
  196. package/test/unit/precheck-story-size-gate.test.ts +1 -0
  197. package/test/unit/precheck-types.test.ts +1 -0
  198. package/test/unit/prompts.test.ts +1 -0
  199. package/test/unit/rectification.test.ts +2 -1
  200. package/test/unit/registry.test.ts +1 -0
  201. package/test/unit/routing/routing-stability.test.ts +2 -1
  202. package/test/unit/routing/strategies/llm.test.ts +251 -0
  203. package/test/unit/routing-advanced.test.ts +313 -0
  204. package/test/unit/routing-core.test.ts +341 -0
  205. package/test/unit/routing-strategies.test.ts +442 -0
  206. package/test/unit/storyid-events.test.ts +1 -0
  207. package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
  208. package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
  209. package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
  210. package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
  211. package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
  212. package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
  213. package/test/unit/verdict.test.ts +1 -0
  214. package/test/unit/verification/orchestrator-types.test.ts +54 -0
  215. package/test/unit/verification/orchestrator.test.ts +66 -0
  216. package/test/unit/verification/smart-runner-config.test.ts +1 -0
  217. package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
  218. package/test/unit/verification/strategies/acceptance.test.ts +33 -0
  219. package/test/unit/verification/strategies/regression.test.ts +87 -0
  220. package/test/unit/verification/strategies/scoped.test.ts +100 -0
  221. package/test/unit/worktree-manager.test.ts +1 -0
  222. package/src/execution/lifecycle/story-hooks.ts +0 -38
  223. package/src/execution/post-verify.ts +0 -193
  224. package/src/execution/rectification.ts +0 -13
  225. package/src/execution/verification.ts +0 -72
  226. package/test/integration/rectification-flow.test.ts +0 -512
  227. package/test/integration/runner.test.ts +0 -1679
  228. package/test/integration/tdd-orchestrator.test.ts +0 -1762
  229. package/test/unit/execution/post-verify-regression.test.ts +0 -362
  230. package/test/unit/execution/post-verify.test.ts +0 -236
  231. package/test/unit/routing.test.ts +0 -1039
  232. /package/test/{integration → helpers}/helpers.test.ts +0 -0
  233. /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
@@ -0,0 +1,107 @@
1
+ // RE-ARCH: keep
2
+ /**
3
+ * Tests for ClaudeCodeAdapter.runOnce() timeout behavior
4
+ *
5
+ * Covers: US-001 - runOnce() SIGKILL follow-up after grace period
6
+ * - SIGTERM is sent first on timeout
7
+ * - PID is always unregistered in finally block, even if kill() throws
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { ClaudeCodeAdapter, _runOnceDeps } from "../../../src/agents/claude";
15
+ import type { AgentRunOptions } from "../../../src/agents/types";
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Test adapter — overrides buildCommand to avoid requiring the claude binary
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ class TestAdapter extends ClaudeCodeAdapter {
22
+ private readonly testCmd: string[];
23
+
24
+ constructor(cmd: string[]) {
25
+ super();
26
+ this.testCmd = cmd;
27
+ }
28
+
29
+ override buildCommand(_options: AgentRunOptions): string[] {
30
+ return this.testCmd;
31
+ }
32
+ }
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Helpers
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ function makeRunOptions(workdir: string, timeoutSeconds: number): AgentRunOptions {
39
+ return {
40
+ workdir,
41
+ prompt: "test",
42
+ modelTier: "balanced",
43
+ modelDef: { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
44
+ timeoutSeconds,
45
+ };
46
+ }
47
+
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+ // Tests
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+
52
+ describe("runOnce() timeout behavior", () => {
53
+ let tempDir: string;
54
+ const origKillProc = _runOnceDeps.killProc;
55
+
56
+ beforeEach(() => {
57
+ tempDir = mkdtempSync(join(tmpdir(), "nax-claude-test-"));
58
+ });
59
+
60
+ afterEach(() => {
61
+ // Restore original killProc after each test
62
+ _runOnceDeps.killProc = origKillProc;
63
+ if (existsSync(tempDir)) {
64
+ rmSync(tempDir, { recursive: true });
65
+ }
66
+ });
67
+
68
+ test("timeout path sends SIGTERM to process first", async () => {
69
+ const sentSignals: string[] = [];
70
+
71
+ // Record signals sent but still kill the process so proc.exited resolves
72
+ _runOnceDeps.killProc = (proc, signal) => {
73
+ sentSignals.push(String(signal));
74
+ origKillProc(proc, signal);
75
+ };
76
+
77
+ // Long-running process: will be killed by the 100ms timeout
78
+ const adapter = new TestAdapter(["/bin/sh", "-c", "sleep 100"]);
79
+ const result = await adapter.run(makeRunOptions(tempDir, 0.1));
80
+
81
+ expect(result.exitCode).toBe(124); // timeout exit code
82
+ expect(sentSignals[0]).toBe("SIGTERM"); // SIGTERM sent first
83
+ });
84
+
85
+ test("timeout path: unregisters PID even if killProc throws", async () => {
86
+ // Override killProc to throw — simulates kill() failing (e.g., process already gone)
87
+ _runOnceDeps.killProc = (_proc, _signal) => {
88
+ throw new Error("kill failed");
89
+ };
90
+
91
+ // Use a short-lived process (0.5s) with a timeout that fires first (50ms).
92
+ // killProc throws (process not killed), so proc exits naturally at ~0.5s.
93
+ // The finally block must still call unregister regardless.
94
+ const adapter = new TestAdapter(["/bin/sh", "-c", "sleep 0.5"]);
95
+ const options = makeRunOptions(tempDir, 0.05); // 50ms timeout
96
+
97
+ // Should not throw — kill errors are caught internally
98
+ await adapter.run(options);
99
+
100
+ // PID must have been unregistered (file empty or absent)
101
+ const pidsFile = join(tempDir, ".nax-pids");
102
+ if (existsSync(pidsFile)) {
103
+ const content = await Bun.file(pidsFile).text();
104
+ expect(content.trim()).toBe("");
105
+ }
106
+ });
107
+ });
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for LLM Classifier
3
4
  */
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for context auto-detection (BUG-006)
3
4
  */
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for src/cli/status.ts - Feature status display with active run detection
3
4
  */
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for src/commands/common.ts
3
4
  */
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Unit tests for nax logs command
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Unit tests for the nax unlock command
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * DEFAULT_CONFIG.review.checks default value tests
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * RegressionGateConfigSchema — mode and maxRectificationAttempts fields
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Smart Test Runner Config Flag Tests (STR-004)
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Constitution Generators Tests
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Constitution system tests
3
4
  */
@@ -0,0 +1,297 @@
1
+ // RE-ARCH: keep
2
+ /**
3
+ * Tests for context builder module
4
+ */
5
+
6
+ // RE-ARCH: keep
7
+ /**
8
+ * Tests for context builder module
9
+ */
10
+
11
+ import { describe, expect, test } from "bun:test";
12
+ import fs from "node:fs/promises";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import {
16
+ buildContext,
17
+ createDependencyContext,
18
+ createErrorContext,
19
+ createFileContext,
20
+ createProgressContext,
21
+ createStoryContext,
22
+ estimateTokens,
23
+ formatContextAsMarkdown,
24
+ sortContextElements,
25
+ } from "../../../src/context/builder";
26
+ import type { ContextBudget, ContextElement, StoryContext } from "../../../src/context/types";
27
+ import type { PRD, UserStory } from "../../../src/prd";
28
+
29
+ // Helper to create test PRD
30
+ const createTestPRD = (stories: Partial<UserStory>[]): PRD => ({
31
+ project: "test-project",
32
+ feature: "test-feature",
33
+ branchName: "test-branch",
34
+ createdAt: new Date().toISOString(),
35
+ updatedAt: new Date().toISOString(),
36
+ userStories: stories.map((s, i) => ({
37
+ id: s.id || `US-${String(i + 1).padStart(3, "0")}`,
38
+ title: s.title || "Test Story",
39
+ description: s.description || "Test description",
40
+ acceptanceCriteria: s.acceptanceCriteria || ["AC1"],
41
+ dependencies: s.dependencies || [],
42
+ tags: s.tags || [],
43
+ status: s.status || "pending",
44
+ passes: s.passes ?? false,
45
+ escalations: s.escalations || [],
46
+ attempts: s.attempts || 0,
47
+ routing: s.routing,
48
+ priorErrors: s.priorErrors,
49
+ relevantFiles: s.relevantFiles,
50
+ contextFiles: s.contextFiles,
51
+ expectedFiles: s.expectedFiles,
52
+ })),
53
+ });
54
+
55
+ describe("Context Builder", () => {
56
+ describe("context auto-detection (BUG-006)", () => {
57
+ test("should auto-detect files when contextFiles is empty", async () => {
58
+ // Create temp git repo
59
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
60
+
61
+ try {
62
+ // Initialize git
63
+ await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
64
+ await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
65
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
66
+
67
+ // Create files matching story keywords
68
+ await fs.mkdir(path.join(tempDir, "src/routing"), { recursive: true });
69
+ await fs.writeFile(path.join(tempDir, "src/routing/router.ts"), "export class Router { /* routing logic */ }");
70
+ await fs.writeFile(
71
+ path.join(tempDir, "src/routing/chain.ts"),
72
+ "export class RouterChain { /* chain logic */ }",
73
+ );
74
+
75
+ // Commit so git grep can find them
76
+ await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
77
+ await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
78
+
79
+ const prd = createTestPRD([
80
+ {
81
+ id: "US-001",
82
+ title: "Fix routing chain bug",
83
+ description: "Fix issue in router chain",
84
+ acceptanceCriteria: ["Router chain works correctly"],
85
+ // No contextFiles - should auto-detect
86
+ },
87
+ ]);
88
+
89
+ const storyContext: StoryContext = {
90
+ prd,
91
+ currentStoryId: "US-001",
92
+ workdir: tempDir,
93
+ config: {
94
+ context: {
95
+ autoDetect: {
96
+ enabled: true,
97
+ maxFiles: 5,
98
+ traceImports: false,
99
+ },
100
+ testCoverage: {
101
+ enabled: false, // Disable to isolate auto-detect test
102
+ },
103
+ },
104
+ } as any,
105
+ };
106
+
107
+ const budget: ContextBudget = {
108
+ maxTokens: 10000,
109
+ reservedForInstructions: 1000,
110
+ availableForContext: 9000,
111
+ };
112
+
113
+ const built = await buildContext(storyContext, budget);
114
+ const fileElements = built.elements.filter((e) => e.type === "file");
115
+
116
+ // Should auto-detect routing files
117
+ expect(fileElements.length).toBeGreaterThan(0);
118
+ const filePaths = fileElements.map((e) => e.filePath);
119
+ expect(filePaths).toContain("src/routing/router.ts");
120
+ expect(filePaths).toContain("src/routing/chain.ts");
121
+ } finally {
122
+ await fs.rm(tempDir, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ test("should skip auto-detection when contextFiles is provided", async () => {
127
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
128
+
129
+ try {
130
+ await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
131
+ await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
132
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
133
+
134
+ await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
135
+ await fs.writeFile(path.join(tempDir, "src/explicit.ts"), "export const explicit = true;");
136
+ await fs.writeFile(path.join(tempDir, "src/routing.ts"), "export const routing = true;");
137
+
138
+ await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
139
+ await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
140
+
141
+ const prd = createTestPRD([
142
+ {
143
+ id: "US-001",
144
+ title: "Fix routing bug",
145
+ description: "Fix routing",
146
+ acceptanceCriteria: ["Works"],
147
+ contextFiles: ["src/explicit.ts"], // Explicit file provided
148
+ },
149
+ ]);
150
+
151
+ const storyContext: StoryContext = {
152
+ prd,
153
+ currentStoryId: "US-001",
154
+ workdir: tempDir,
155
+ config: {
156
+ context: {
157
+ autoDetect: {
158
+ enabled: true,
159
+ maxFiles: 5,
160
+ traceImports: false,
161
+ },
162
+ testCoverage: {
163
+ enabled: false,
164
+ },
165
+ },
166
+ } as any,
167
+ };
168
+
169
+ const budget: ContextBudget = {
170
+ maxTokens: 10000,
171
+ reservedForInstructions: 1000,
172
+ availableForContext: 9000,
173
+ };
174
+
175
+ const built = await buildContext(storyContext, budget);
176
+ const fileElements = built.elements.filter((e) => e.type === "file");
177
+
178
+ // Should only load explicit file, NOT auto-detect
179
+ expect(fileElements.length).toBe(1);
180
+ expect(fileElements[0].filePath).toBe("src/explicit.ts");
181
+ } finally {
182
+ await fs.rm(tempDir, { recursive: true, force: true });
183
+ }
184
+ });
185
+
186
+ test("should skip auto-detection when disabled in config", async () => {
187
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
188
+
189
+ try {
190
+ await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
191
+ await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
192
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
193
+
194
+ await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
195
+ await fs.writeFile(path.join(tempDir, "src/routing.ts"), "export const routing = true;");
196
+
197
+ await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
198
+ await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
199
+
200
+ const prd = createTestPRD([
201
+ {
202
+ id: "US-001",
203
+ title: "Fix routing bug",
204
+ description: "Fix routing",
205
+ acceptanceCriteria: ["Works"],
206
+ // No contextFiles
207
+ },
208
+ ]);
209
+
210
+ const storyContext: StoryContext = {
211
+ prd,
212
+ currentStoryId: "US-001",
213
+ workdir: tempDir,
214
+ config: {
215
+ context: {
216
+ autoDetect: {
217
+ enabled: false, // Disabled
218
+ maxFiles: 5,
219
+ traceImports: false,
220
+ },
221
+ testCoverage: {
222
+ enabled: false,
223
+ },
224
+ },
225
+ } as any,
226
+ };
227
+
228
+ const budget: ContextBudget = {
229
+ maxTokens: 10000,
230
+ reservedForInstructions: 1000,
231
+ availableForContext: 9000,
232
+ };
233
+
234
+ const built = await buildContext(storyContext, budget);
235
+ const fileElements = built.elements.filter((e) => e.type === "file");
236
+
237
+ // Should NOT auto-detect when disabled
238
+ expect(fileElements.length).toBe(0);
239
+ } finally {
240
+ await fs.rm(tempDir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test("should handle auto-detection failure gracefully", async () => {
245
+ // Non-git directory - git grep will fail
246
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
247
+
248
+ try {
249
+ await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
250
+ await fs.writeFile(path.join(tempDir, "src/file.ts"), "export const test = true;");
251
+
252
+ const prd = createTestPRD([
253
+ {
254
+ id: "US-001",
255
+ title: "Fix test bug",
256
+ description: "Fix test",
257
+ acceptanceCriteria: ["Works"],
258
+ // No contextFiles
259
+ },
260
+ ]);
261
+
262
+ const storyContext: StoryContext = {
263
+ prd,
264
+ currentStoryId: "US-001",
265
+ workdir: tempDir,
266
+ config: {
267
+ context: {
268
+ autoDetect: {
269
+ enabled: true,
270
+ maxFiles: 5,
271
+ traceImports: false,
272
+ },
273
+ testCoverage: {
274
+ enabled: false,
275
+ },
276
+ },
277
+ } as any,
278
+ };
279
+
280
+ const budget: ContextBudget = {
281
+ maxTokens: 10000,
282
+ reservedForInstructions: 1000,
283
+ availableForContext: 9000,
284
+ };
285
+
286
+ // Should not throw, just log warning and continue
287
+ const built = await buildContext(storyContext, budget);
288
+ const fileElements = built.elements.filter((e) => e.type === "file");
289
+
290
+ // No files loaded (graceful failure)
291
+ expect(fileElements.length).toBe(0);
292
+ } finally {
293
+ await fs.rm(tempDir, { recursive: true, force: true });
294
+ }
295
+ });
296
+ });
297
+ });