@nathapp/nax 0.21.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 (210) hide show
  1. package/.mcp.json +8 -0
  2. package/docs/ROADMAP.md +20 -5
  3. package/docs/adr/ADR-005-implementation-plan.md +655 -0
  4. package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
  5. package/package.json +1 -1
  6. package/src/agents/claude.ts +44 -9
  7. package/src/config/types.ts +11 -0
  8. package/src/execution/dry-run.ts +81 -0
  9. package/src/execution/escalation/tier-outcome.ts +29 -44
  10. package/src/execution/executor-types.ts +65 -0
  11. package/src/execution/index.ts +0 -17
  12. package/src/execution/iteration-runner.ts +132 -0
  13. package/src/execution/lifecycle/index.ts +0 -1
  14. package/src/execution/lifecycle/run-regression.ts +5 -5
  15. package/src/execution/pipeline-result-handler.ts +51 -254
  16. package/src/execution/sequential-executor.ts +72 -316
  17. package/src/execution/story-selector.ts +75 -0
  18. package/src/pipeline/event-bus.ts +276 -0
  19. package/src/pipeline/runner.ts +51 -77
  20. package/src/pipeline/stages/autofix.ts +133 -0
  21. package/src/pipeline/stages/completion.ts +22 -30
  22. package/src/pipeline/stages/index.ts +30 -13
  23. package/src/pipeline/stages/rectify.ts +93 -0
  24. package/src/pipeline/stages/regression.ts +88 -0
  25. package/src/pipeline/stages/review.ts +19 -153
  26. package/src/pipeline/stages/verify.ts +18 -2
  27. package/src/pipeline/subscribers/hooks.ts +133 -0
  28. package/src/pipeline/subscribers/interaction.ts +68 -0
  29. package/src/pipeline/subscribers/reporters.ts +174 -0
  30. package/src/pipeline/types.ts +10 -1
  31. package/src/review/orchestrator.ts +105 -0
  32. package/src/tdd/prompts.ts +1 -1
  33. package/src/verification/index.ts +1 -1
  34. package/src/verification/orchestrator-types.ts +145 -0
  35. package/src/verification/orchestrator.ts +76 -0
  36. package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
  37. package/src/verification/{gate.ts → runners.ts} +17 -105
  38. package/src/verification/strategies/acceptance.ts +133 -0
  39. package/src/verification/strategies/regression.ts +90 -0
  40. package/src/verification/strategies/scoped.ts +123 -0
  41. package/test/COVERAGE-GAPS.md +333 -0
  42. package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
  43. package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
  44. package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
  45. package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
  46. package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
  47. package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
  48. package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
  49. package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
  50. package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
  51. package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
  52. package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
  53. package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
  54. package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
  55. package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
  56. package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
  57. package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
  58. package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
  59. package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
  60. package/test/integration/config/merger.test.ts +1 -0
  61. package/test/integration/config/paths.test.ts +1 -0
  62. package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
  63. package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
  64. package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
  65. package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
  66. package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
  67. package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
  68. package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
  69. package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
  70. package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
  71. package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
  72. package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
  73. package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
  74. package/test/integration/execution/runner-batching.test.ts +682 -0
  75. package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
  76. package/test/integration/execution/runner-escalation.test.ts +561 -0
  77. package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
  78. package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
  79. package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
  80. package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
  81. package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
  82. package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
  83. package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
  84. package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
  85. package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
  86. package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
  87. package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
  88. package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
  89. package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
  90. package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
  91. package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
  92. package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
  93. package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
  94. package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
  95. package/test/integration/plugins/config-integration.test.ts +1 -0
  96. package/test/integration/plugins/config-resolution.test.ts +1 -0
  97. package/test/integration/plugins/loader.test.ts +1 -0
  98. package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
  99. package/test/integration/plugins/validator.test.ts +1 -0
  100. package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
  101. package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
  102. package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
  103. package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
  104. package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
  105. package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +9 -403
  106. package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
  107. package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
  108. package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
  109. package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
  110. package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
  111. package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
  112. package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
  113. package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
  114. package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
  115. package/test/integration/tmp/headless-test/test.jsonl +30 -0
  116. package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
  117. package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
  118. package/test/unit/acceptance.test.ts +1 -0
  119. package/test/unit/agent-stderr-capture.test.ts +1 -0
  120. package/test/unit/agents/claude.test.ts +1 -0
  121. package/test/unit/analyze-classifier.test.ts +1 -0
  122. package/test/unit/auto-detect.test.ts +1 -0
  123. package/test/unit/cli-status.test.ts +1 -0
  124. package/test/unit/commands/common.test.ts +1 -0
  125. package/test/unit/commands/logs.test.ts +1 -0
  126. package/test/unit/commands/unlock.test.ts +1 -0
  127. package/test/unit/config/defaults.test.ts +1 -0
  128. package/test/unit/config/regression-gate-schema.test.ts +1 -0
  129. package/test/unit/config/smart-runner-flag.test.ts +1 -0
  130. package/test/unit/constitution-generators.test.ts +1 -0
  131. package/test/unit/constitution.test.ts +1 -0
  132. package/test/unit/context/context-autodetect.test.ts +297 -0
  133. package/test/unit/context/context-build.test.ts +575 -0
  134. package/test/unit/context/context-coverage.test.ts +236 -0
  135. package/test/unit/context/context-error.test.ts +93 -0
  136. package/test/unit/context/context-estimate-tokens.test.ts +201 -0
  137. package/test/unit/context/context-format.test.ts +302 -0
  138. package/test/unit/context/context-isolation.test.ts +267 -0
  139. package/test/unit/context/context-sort.test.ts +93 -0
  140. package/test/unit/context/context-story.test.ts +108 -0
  141. package/test/{context → unit/context}/prior-failures.test.ts +5 -4
  142. package/test/unit/context.test.ts +1 -0
  143. package/test/unit/crash-recovery.test.ts +1 -0
  144. package/test/unit/escalation.test.ts +1 -0
  145. package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
  146. package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
  147. package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
  148. package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
  149. package/test/unit/execution-logging-stderr.test.ts +1 -0
  150. package/test/unit/execution-stage.test.ts +1 -0
  151. package/test/unit/fix-generator.test.ts +1 -0
  152. package/test/unit/greenfield.test.ts +1 -0
  153. package/test/unit/interaction/human-review-trigger.test.ts +1 -0
  154. package/test/unit/interaction-network-failures.test.ts +1 -0
  155. package/test/unit/interaction-plugins.test.ts +1 -0
  156. package/test/unit/logging/formatter.test.ts +1 -0
  157. package/test/unit/merge.test.ts +1 -0
  158. package/test/unit/pipeline/event-bus.test.ts +105 -0
  159. package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
  160. package/test/unit/pipeline/runner-retry.test.ts +89 -0
  161. package/test/unit/pipeline/stages/autofix.test.ts +97 -0
  162. package/test/unit/pipeline/stages/rectify.test.ts +101 -0
  163. package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
  164. package/test/unit/pipeline/stages/verify.test.ts +1 -0
  165. package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
  166. package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
  167. package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
  168. package/test/unit/pipeline/verify-smart-runner.test.ts +1 -0
  169. package/test/unit/prd-auto-default.test.ts +1 -0
  170. package/test/unit/prd-failure-category.test.ts +1 -0
  171. package/test/unit/prd-get-next-story.test.ts +1 -0
  172. package/test/unit/precheck-checks.test.ts +1 -0
  173. package/test/unit/precheck-story-size-gate.test.ts +1 -0
  174. package/test/unit/precheck-types.test.ts +1 -0
  175. package/test/unit/prompts.test.ts +1 -0
  176. package/test/unit/rectification.test.ts +2 -1
  177. package/test/unit/registry.test.ts +1 -0
  178. package/test/unit/routing/routing-stability.test.ts +1 -0
  179. package/test/unit/routing/strategies/llm.test.ts +1 -0
  180. package/test/unit/routing-advanced.test.ts +313 -0
  181. package/test/unit/routing-core.test.ts +341 -0
  182. package/test/unit/routing-strategies.test.ts +442 -0
  183. package/test/unit/storyid-events.test.ts +1 -0
  184. package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
  185. package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
  186. package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
  187. package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
  188. package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
  189. package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
  190. package/test/unit/verdict.test.ts +1 -0
  191. package/test/unit/verification/orchestrator-types.test.ts +54 -0
  192. package/test/unit/verification/orchestrator.test.ts +66 -0
  193. package/test/unit/verification/smart-runner-config.test.ts +1 -0
  194. package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
  195. package/test/unit/verification/strategies/acceptance.test.ts +33 -0
  196. package/test/unit/verification/strategies/regression.test.ts +87 -0
  197. package/test/unit/verification/strategies/scoped.test.ts +100 -0
  198. package/test/unit/worktree-manager.test.ts +1 -0
  199. package/src/execution/lifecycle/story-hooks.ts +0 -38
  200. package/src/execution/post-verify.ts +0 -193
  201. package/src/execution/rectification.ts +0 -13
  202. package/src/execution/verification.ts +0 -72
  203. package/test/integration/rectification-flow.test.ts +0 -512
  204. package/test/integration/runner.test.ts +0 -1679
  205. package/test/integration/tdd-orchestrator.test.ts +0 -1762
  206. package/test/unit/execution/post-verify-regression.test.ts +0 -362
  207. package/test/unit/execution/post-verify.test.ts +0 -236
  208. package/test/unit/routing.test.ts +0 -1039
  209. /package/test/{integration → helpers}/helpers.test.ts +0 -0
  210. /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
@@ -0,0 +1,536 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import type { AgentAdapter, AgentResult } from "../../../src/agents";
6
+ import { DEFAULT_CONFIG } from "../../../src/config";
7
+ import type { UserStory } from "../../../src/prd";
8
+ import { runThreeSessionTdd } from "../../../src/tdd/orchestrator";
9
+ import { VERDICT_FILE } from "../../../src/tdd/verdict";
10
+
11
+ let originalSpawn: typeof Bun.spawn;
12
+
13
+ beforeEach(() => {
14
+ originalSpawn = Bun.spawn;
15
+ });
16
+
17
+ afterEach(() => {
18
+ Bun.spawn = originalSpawn;
19
+ });
20
+
21
+ /** Create a mock agent that returns sequential results */
22
+ function createMockAgent(results: Partial<AgentResult>[]): AgentAdapter {
23
+ let callCount = 0;
24
+ return {
25
+ name: "mock",
26
+ displayName: "Mock Agent",
27
+ binary: "mock",
28
+ isInstalled: async () => true,
29
+ buildCommand: () => ["mock"],
30
+ run: mock(async () => {
31
+ const r = results[callCount] || {};
32
+ callCount++;
33
+ return {
34
+ success: r.success ?? true,
35
+ exitCode: r.exitCode ?? 0,
36
+ output: r.output ?? "",
37
+ rateLimited: r.rateLimited ?? false,
38
+ durationMs: r.durationMs ?? 100,
39
+ estimatedCost: r.estimatedCost ?? 0.01,
40
+ };
41
+ }),
42
+ };
43
+ }
44
+
45
+ /** Mock Bun.spawn to intercept git commands */
46
+ function mockGitSpawn(opts: {
47
+ /** Files returned by git diff for each session (indexed by git-diff call number) */
48
+ diffFiles: string[][];
49
+ /** Optional: mock test command success (default: true) */
50
+ testCommandSuccess?: boolean;
51
+ }) {
52
+ let revParseCount = 0;
53
+ let diffCount = 0;
54
+ const testSuccess = opts.testCommandSuccess ?? true;
55
+
56
+ // @ts-ignore — mocking global
57
+ Bun.spawn = mock((cmd: string[], spawnOpts?: any) => {
58
+ // Intercept test commands (bun test, npm test, etc.)
59
+ if ((cmd[0] === "/bin/sh" || cmd[0] === "/bin/bash" || cmd[0] === "/bin/zsh") && cmd[1] === "-c") {
60
+ return {
61
+ pid: 9999,
62
+ exited: Promise.resolve(testSuccess ? 0 : 1),
63
+ stdout: new Response(testSuccess ? "tests pass\n" : "tests fail\n").body,
64
+ stderr: new Response("").body,
65
+ };
66
+ }
67
+ if (cmd[0] === "git" && cmd[1] === "rev-parse") {
68
+ revParseCount++;
69
+ return {
70
+ exited: Promise.resolve(0),
71
+ stdout: new Response(`ref-${revParseCount}\n`).body,
72
+ stderr: new Response("").body,
73
+ };
74
+ }
75
+ if (cmd[0] === "git" && cmd[1] === "checkout") {
76
+ // Intercept git checkout (used in zero-file fallback) — silently succeed
77
+ return {
78
+ exited: Promise.resolve(0),
79
+ stdout: new Response("").body,
80
+ stderr: new Response("").body,
81
+ };
82
+ }
83
+ if (cmd[0] === "git" && cmd[1] === "diff") {
84
+ const files = opts.diffFiles[diffCount] || [];
85
+ diffCount++;
86
+ return {
87
+ exited: Promise.resolve(0),
88
+ stdout: new Response(files.join("\n") + "\n").body,
89
+ stderr: new Response("").body,
90
+ };
91
+ }
92
+ return originalSpawn(cmd, spawnOpts);
93
+ });
94
+ }
95
+
96
+ const story: UserStory = {
97
+ id: "US-001",
98
+ title: "Add user validation",
99
+ description: "Add validation to user input",
100
+ acceptanceCriteria: ["Validation works", "Errors are clear"],
101
+ dependencies: [],
102
+ tags: [],
103
+ status: "pending",
104
+ passes: false,
105
+ escalations: [],
106
+ attempts: 0,
107
+ };
108
+
109
+
110
+ describe("runThreeSessionTdd — T9: verdict integration", () => {
111
+ let tmpDir: string;
112
+
113
+ beforeEach(async () => {
114
+ tmpDir = `/tmp/nax-t9-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
115
+ await mkdir(tmpDir, { recursive: true });
116
+ });
117
+
118
+ afterEach(async () => {
119
+ await rm(tmpDir, { recursive: true, force: true });
120
+ Bun.spawn = originalSpawn;
121
+ });
122
+
123
+ /** Write a valid verdict file to tmpDir */
124
+ async function writeVerdictToDir(opts: {
125
+ approved: boolean;
126
+ failReason?: "tests-failing" | "illegitimate-mods" | "criteria-not-met" | "poor-quality";
127
+ }) {
128
+ const verdict = {
129
+ version: 1,
130
+ approved: opts.approved,
131
+ tests: {
132
+ allPassing: opts.failReason !== "tests-failing",
133
+ passCount: opts.failReason === "tests-failing" ? 5 : 10,
134
+ failCount: opts.failReason === "tests-failing" ? 3 : 0,
135
+ },
136
+ testModifications: {
137
+ detected: opts.failReason === "illegitimate-mods",
138
+ files: opts.failReason === "illegitimate-mods" ? ["test/foo.test.ts"] : [],
139
+ legitimate: opts.failReason !== "illegitimate-mods",
140
+ reasoning: opts.failReason === "illegitimate-mods" ? "Implementer cheated" : "No mods",
141
+ },
142
+ acceptanceCriteria: {
143
+ allMet: opts.failReason !== "criteria-not-met",
144
+ criteria:
145
+ opts.failReason === "criteria-not-met"
146
+ ? [{ criterion: "Must work", met: false }]
147
+ : [{ criterion: "Works", met: true }],
148
+ },
149
+ quality: {
150
+ rating: opts.failReason === "poor-quality" ? "poor" : "good",
151
+ issues: opts.failReason === "poor-quality" ? ["Security issue"] : [],
152
+ },
153
+ fixes: [],
154
+ reasoning: opts.approved ? "All good." : "Implementation rejected.",
155
+ };
156
+ await writeFile(path.join(tmpDir, VERDICT_FILE), JSON.stringify(verdict, null, 2));
157
+ }
158
+
159
+ /**
160
+ * Mock Bun.spawn for a full 3-session T9 run.
161
+ * Provides 6 git diff calls (isolation + getChangedFiles per session)
162
+ * and optionally intercepts the post-TDD shell command (bun test).
163
+ */
164
+ function mockGitAndTestForT9(opts: {
165
+ diffFiles?: string[][];
166
+ onTestCmd?: () => { exitCode: number; stdout: string };
167
+ }) {
168
+ const files = opts.diffFiles ?? [
169
+ ["test/user.test.ts"], // s1 isolation
170
+ ["test/user.test.ts"], // s1 getChangedFiles
171
+ ["src/user.ts"], // s2 isolation
172
+ ["src/user.ts"], // s2 getChangedFiles
173
+ [], // s3 isolation
174
+ ["src/user.ts"], // s3 getChangedFiles
175
+ ];
176
+ let revParseCount = 0;
177
+ let diffCount = 0;
178
+
179
+ // @ts-ignore — mocking global
180
+ Bun.spawn = mock((cmd: string[], spawnOpts?: any) => {
181
+ if (cmd[0] === "/bin/sh" && cmd[2]?.includes("bun test")) {
182
+ const r = opts.onTestCmd?.() ?? { exitCode: 0, stdout: "5 pass, 0 fail\n" };
183
+ return {
184
+ pid: 9999,
185
+ exited: Promise.resolve(r.exitCode),
186
+ stdout: new Response(r.stdout).body,
187
+ stderr: new Response("").body,
188
+ };
189
+ }
190
+ if (cmd[0] === "git" && cmd[1] === "rev-parse") {
191
+ revParseCount++;
192
+ return {
193
+ exited: Promise.resolve(0),
194
+ stdout: new Response(`ref-${revParseCount}\n`).body,
195
+ stderr: new Response("").body,
196
+ };
197
+ }
198
+ if (cmd[0] === "git" && cmd[1] === "diff") {
199
+ const f = files[diffCount] || [];
200
+ diffCount++;
201
+ return {
202
+ exited: Promise.resolve(0),
203
+ stdout: new Response(f.join("\n") + "\n").body,
204
+ stderr: new Response("").body,
205
+ };
206
+ }
207
+ return originalSpawn(cmd, spawnOpts);
208
+ });
209
+ }
210
+
211
+ test("verdict approved=true: overall success even when verifier session failed", async () => {
212
+ await writeVerdictToDir({ approved: true });
213
+ mockGitAndTestForT9({});
214
+
215
+ const agent = createMockAgent([
216
+ { success: true, estimatedCost: 0.01 },
217
+ { success: true, estimatedCost: 0.02 },
218
+ { success: false, exitCode: 1, estimatedCost: 0.01 }, // verifier exits non-zero
219
+ ]);
220
+
221
+ const result = await runThreeSessionTdd({
222
+ agent,
223
+ story,
224
+ config: DEFAULT_CONFIG,
225
+ workdir: tmpDir,
226
+ modelTier: "balanced",
227
+ });
228
+
229
+ expect(result.success).toBe(true);
230
+ expect(result.needsHumanReview).toBe(false);
231
+ expect(result.failureCategory).toBeUndefined();
232
+ expect(result.reviewReason).toBeUndefined();
233
+ });
234
+
235
+ test("verdict approved=true: skips the post-TDD independent test check", async () => {
236
+ await writeVerdictToDir({ approved: true });
237
+ let testCommandCalled = false;
238
+ mockGitAndTestForT9({
239
+ onTestCmd: () => {
240
+ testCommandCalled = true;
241
+ return { exitCode: 0, stdout: "" };
242
+ },
243
+ });
244
+
245
+ const agent = createMockAgent([
246
+ { success: true, estimatedCost: 0.01 },
247
+ { success: true, estimatedCost: 0.02 },
248
+ { success: false, exitCode: 1, estimatedCost: 0.01 }, // verifier fails
249
+ ]);
250
+
251
+ // Disable rectification to avoid test command being called for full-suite gate
252
+ const configNoRectification = {
253
+ ...DEFAULT_CONFIG,
254
+ execution: {
255
+ ...DEFAULT_CONFIG.execution,
256
+ rectification: { ...DEFAULT_CONFIG.execution.rectification, enabled: false },
257
+ },
258
+ };
259
+
260
+ await runThreeSessionTdd({
261
+ agent,
262
+ story,
263
+ config: configNoRectification,
264
+ workdir: tmpDir,
265
+ modelTier: "balanced",
266
+ });
267
+ expect(testCommandCalled).toBe(false); // Test was NOT run when verdict present
268
+ });
269
+
270
+ test("verdict approved=false + tests-failing → failureCategory='tests-failing'", async () => {
271
+ await writeVerdictToDir({ approved: false, failReason: "tests-failing" });
272
+ mockGitAndTestForT9({});
273
+
274
+ const agent = createMockAgent([
275
+ { success: true, estimatedCost: 0.01 },
276
+ { success: true, estimatedCost: 0.02 },
277
+ { success: true, estimatedCost: 0.01 }, // sessions succeed but verdict says rejected
278
+ ]);
279
+
280
+ const result = await runThreeSessionTdd({
281
+ agent,
282
+ story,
283
+ config: DEFAULT_CONFIG,
284
+ workdir: tmpDir,
285
+ modelTier: "balanced",
286
+ });
287
+
288
+ expect(result.success).toBe(false);
289
+ expect(result.needsHumanReview).toBe(true);
290
+ expect(result.failureCategory).toBe("tests-failing");
291
+ expect(result.reviewReason).toContain("failure(s)");
292
+ });
293
+
294
+ test("verdict approved=false + illegitimate test mods → failureCategory='verifier-rejected'", async () => {
295
+ await writeVerdictToDir({ approved: false, failReason: "illegitimate-mods" });
296
+ mockGitAndTestForT9({});
297
+
298
+ const agent = createMockAgent([
299
+ { success: true, estimatedCost: 0.01 },
300
+ { success: true, estimatedCost: 0.02 },
301
+ { success: true, estimatedCost: 0.01 },
302
+ ]);
303
+
304
+ const result = await runThreeSessionTdd({
305
+ agent,
306
+ story,
307
+ config: DEFAULT_CONFIG,
308
+ workdir: tmpDir,
309
+ modelTier: "balanced",
310
+ });
311
+
312
+ expect(result.success).toBe(false);
313
+ expect(result.failureCategory).toBe("verifier-rejected");
314
+ expect(result.reviewReason).toContain("illegitimate test modifications");
315
+ });
316
+
317
+ test("verdict approved=false + criteria not met → failureCategory='verifier-rejected'", async () => {
318
+ await writeVerdictToDir({ approved: false, failReason: "criteria-not-met" });
319
+ mockGitAndTestForT9({});
320
+
321
+ const agent = createMockAgent([
322
+ { success: true, estimatedCost: 0.01 },
323
+ { success: true, estimatedCost: 0.02 },
324
+ { success: true, estimatedCost: 0.01 },
325
+ ]);
326
+
327
+ const result = await runThreeSessionTdd({
328
+ agent,
329
+ story,
330
+ config: DEFAULT_CONFIG,
331
+ workdir: tmpDir,
332
+ modelTier: "balanced",
333
+ });
334
+
335
+ expect(result.success).toBe(false);
336
+ expect(result.failureCategory).toBe("verifier-rejected");
337
+ expect(result.reviewReason).toContain("Must work");
338
+ });
339
+
340
+ test("no verdict file → fallback: post-TDD test check is run on session failures", async () => {
341
+ // No verdict file — when verifier fails, falls back to running tests independently
342
+ let testCommandCalled = false;
343
+ mockGitAndTestForT9({
344
+ onTestCmd: () => {
345
+ testCommandCalled = true;
346
+ return { exitCode: 0, stdout: "5 pass, 0 fail\n" }; // Tests pass in fallback
347
+ },
348
+ });
349
+
350
+ const agent = createMockAgent([
351
+ { success: true, estimatedCost: 0.01 },
352
+ { success: true, estimatedCost: 0.02 },
353
+ { success: false, exitCode: 1, estimatedCost: 0.01 }, // verifier fails
354
+ ]);
355
+
356
+ const result = await runThreeSessionTdd({
357
+ agent,
358
+ story,
359
+ config: DEFAULT_CONFIG,
360
+ workdir: tmpDir,
361
+ modelTier: "balanced",
362
+ });
363
+
364
+ expect(testCommandCalled).toBe(true); // Fallback test run was executed
365
+ expect(result.success).toBe(true); // Tests pass in fallback → success
366
+ expect(result.verdict).toBeNull(); // No verdict available
367
+ });
368
+
369
+ test("malformed verdict → fallback: post-TDD test check is run", async () => {
370
+ // Write invalid JSON — should trigger fallback
371
+ await writeFile(path.join(tmpDir, VERDICT_FILE), "{ this is not valid json }");
372
+ let testCommandCalled = false;
373
+ mockGitAndTestForT9({
374
+ onTestCmd: () => {
375
+ testCommandCalled = true;
376
+ return { exitCode: 0, stdout: "5 pass\n" };
377
+ },
378
+ });
379
+
380
+ const agent = createMockAgent([
381
+ { success: true, estimatedCost: 0.01 },
382
+ { success: true, estimatedCost: 0.02 },
383
+ { success: false, exitCode: 1, estimatedCost: 0.01 },
384
+ ]);
385
+
386
+ const result = await runThreeSessionTdd({
387
+ agent,
388
+ story,
389
+ config: DEFAULT_CONFIG,
390
+ workdir: tmpDir,
391
+ modelTier: "balanced",
392
+ });
393
+
394
+ expect(testCommandCalled).toBe(true); // Fallback used when verdict is malformed
395
+ expect(result.verdict).toBeNull(); // Malformed = null
396
+ });
397
+
398
+ test("verdict stored in result.verdict for logging/debugging (approved=true)", async () => {
399
+ await writeVerdictToDir({ approved: true });
400
+ mockGitAndTestForT9({});
401
+
402
+ const agent = createMockAgent([
403
+ { success: true, estimatedCost: 0.01 },
404
+ { success: true, estimatedCost: 0.02 },
405
+ { success: true, estimatedCost: 0.01 },
406
+ ]);
407
+
408
+ const result = await runThreeSessionTdd({
409
+ agent,
410
+ story,
411
+ config: DEFAULT_CONFIG,
412
+ workdir: tmpDir,
413
+ modelTier: "balanced",
414
+ });
415
+
416
+ expect(result.verdict).toBeDefined();
417
+ expect(result.verdict).not.toBeNull();
418
+ expect(result.verdict!.version).toBe(1);
419
+ expect(result.verdict!.approved).toBe(true);
420
+ expect(result.verdict!.tests.allPassing).toBe(true);
421
+ expect(result.verdict!.tests.passCount).toBe(10);
422
+ expect(result.verdict!.reasoning).toBe("All good.");
423
+ });
424
+
425
+ test("verdict stored in result.verdict for logging/debugging (approved=false)", async () => {
426
+ await writeVerdictToDir({ approved: false, failReason: "tests-failing" });
427
+ mockGitAndTestForT9({});
428
+
429
+ const agent = createMockAgent([
430
+ { success: true, estimatedCost: 0.01 },
431
+ { success: true, estimatedCost: 0.02 },
432
+ { success: true, estimatedCost: 0.01 },
433
+ ]);
434
+
435
+ const result = await runThreeSessionTdd({
436
+ agent,
437
+ story,
438
+ config: DEFAULT_CONFIG,
439
+ workdir: tmpDir,
440
+ modelTier: "balanced",
441
+ });
442
+
443
+ expect(result.verdict).not.toBeNull();
444
+ expect(result.verdict!.approved).toBe(false);
445
+ expect(result.verdict!.tests.failCount).toBe(3);
446
+ });
447
+
448
+ test("verdict file is deleted after reading (cleanup enforced)", async () => {
449
+ await writeVerdictToDir({ approved: true });
450
+ mockGitAndTestForT9({});
451
+
452
+ const verdictPath = path.join(tmpDir, VERDICT_FILE);
453
+ expect(existsSync(verdictPath)).toBe(true); // File exists before run
454
+
455
+ const agent = createMockAgent([
456
+ { success: true, estimatedCost: 0.01 },
457
+ { success: true, estimatedCost: 0.02 },
458
+ { success: true, estimatedCost: 0.01 },
459
+ ]);
460
+ await runThreeSessionTdd({
461
+ agent,
462
+ story,
463
+ config: DEFAULT_CONFIG,
464
+ workdir: tmpDir,
465
+ modelTier: "balanced",
466
+ });
467
+
468
+ expect(existsSync(verdictPath)).toBe(false); // File cleaned up after run
469
+ });
470
+
471
+ test("no verdict + all sessions succeed → success without running test check", async () => {
472
+ // All sessions succeed, no verdict → should succeed and NOT run the test command
473
+ let testCommandCalled = false;
474
+ mockGitAndTestForT9({
475
+ onTestCmd: () => {
476
+ testCommandCalled = true;
477
+ return { exitCode: 0, stdout: "" };
478
+ },
479
+ });
480
+
481
+ const agent = createMockAgent([
482
+ { success: true, estimatedCost: 0.01 },
483
+ { success: true, estimatedCost: 0.02 },
484
+ { success: true, estimatedCost: 0.01 },
485
+ ]);
486
+
487
+ // Disable rectification to avoid test command being called for full-suite gate
488
+ const configNoRectification = {
489
+ ...DEFAULT_CONFIG,
490
+ execution: {
491
+ ...DEFAULT_CONFIG.execution,
492
+ rectification: { ...DEFAULT_CONFIG.execution.rectification, enabled: false },
493
+ },
494
+ };
495
+
496
+ const result = await runThreeSessionTdd({
497
+ agent,
498
+ story,
499
+ config: configNoRectification,
500
+ workdir: tmpDir,
501
+ modelTier: "balanced",
502
+ });
503
+
504
+ expect(result.success).toBe(true);
505
+ expect(testCommandCalled).toBe(false); // Not needed when sessions all succeed
506
+ expect(result.verdict).toBeNull(); // No verdict
507
+ expect(result.failureCategory).toBeUndefined();
508
+ });
509
+
510
+ test("early-exit before session 3 (session 1 fails) → verdict is undefined (not attempted)", async () => {
511
+ // If we exit before session 3, verdict reading is never attempted
512
+ mockGitAndTestForT9({
513
+ diffFiles: [
514
+ ["test/user.test.ts"], // s1 isolation
515
+ ["test/user.test.ts"], // s1 getChangedFiles
516
+ ],
517
+ });
518
+
519
+ const agent = createMockAgent([
520
+ { success: false, exitCode: 1, estimatedCost: 0.01 }, // session 1 fails
521
+ ]);
522
+
523
+ const result = await runThreeSessionTdd({
524
+ agent,
525
+ story,
526
+ config: DEFAULT_CONFIG,
527
+ workdir: tmpDir,
528
+ modelTier: "balanced",
529
+ });
530
+
531
+ expect(result.success).toBe(false);
532
+ expect(result.sessions).toHaveLength(1);
533
+ // verdict is undefined (field not set) because we never got to session 3
534
+ expect(result.verdict).toBeUndefined();
535
+ });
536
+ });
@@ -0,0 +1,30 @@
1
+ {"timestamp":"2026-03-06T08:58:27.466Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
2
+ {"timestamp":"2026-03-06T08:58:27.482Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
3
+ {"timestamp":"2026-03-06T08:58:27.482Z","level":"debug","stage":"test.stage","message":"Debug message"}
4
+ {"timestamp":"2026-03-06T08:58:27.482Z","level":"info","stage":"test.stage","message":"Info message"}
5
+ {"timestamp":"2026-03-06T08:58:27.482Z","level":"error","stage":"test.stage","message":"Error message"}
6
+ {"timestamp":"2026-03-06T08:58:27.483Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
7
+ {"timestamp":"2026-03-06T09:00:48.487Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
8
+ {"timestamp":"2026-03-06T09:00:48.520Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
9
+ {"timestamp":"2026-03-06T09:00:48.521Z","level":"debug","stage":"test.stage","message":"Debug message"}
10
+ {"timestamp":"2026-03-06T09:00:48.521Z","level":"info","stage":"test.stage","message":"Info message"}
11
+ {"timestamp":"2026-03-06T09:00:48.521Z","level":"error","stage":"test.stage","message":"Error message"}
12
+ {"timestamp":"2026-03-06T09:00:48.521Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
13
+ {"timestamp":"2026-03-06T09:03:16.966Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
14
+ {"timestamp":"2026-03-06T09:03:16.981Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
15
+ {"timestamp":"2026-03-06T09:03:16.981Z","level":"debug","stage":"test.stage","message":"Debug message"}
16
+ {"timestamp":"2026-03-06T09:03:16.981Z","level":"info","stage":"test.stage","message":"Info message"}
17
+ {"timestamp":"2026-03-06T09:03:16.981Z","level":"error","stage":"test.stage","message":"Error message"}
18
+ {"timestamp":"2026-03-06T09:03:16.982Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
19
+ {"timestamp":"2026-03-06T09:08:50.110Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
20
+ {"timestamp":"2026-03-06T09:08:50.143Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
21
+ {"timestamp":"2026-03-06T09:08:50.144Z","level":"debug","stage":"test.stage","message":"Debug message"}
22
+ {"timestamp":"2026-03-06T09:08:50.144Z","level":"info","stage":"test.stage","message":"Info message"}
23
+ {"timestamp":"2026-03-06T09:08:50.144Z","level":"error","stage":"test.stage","message":"Error message"}
24
+ {"timestamp":"2026-03-06T09:08:50.144Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
25
+ {"timestamp":"2026-03-06T10:13:12.262Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
26
+ {"timestamp":"2026-03-06T10:13:12.263Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
27
+ {"timestamp":"2026-03-06T10:13:12.263Z","level":"debug","stage":"test.stage","message":"Debug message"}
28
+ {"timestamp":"2026-03-06T10:13:12.263Z","level":"info","stage":"test.stage","message":"Info message"}
29
+ {"timestamp":"2026-03-06T10:13:12.263Z","level":"error","stage":"test.stage","message":"Error message"}
30
+ {"timestamp":"2026-03-06T10:13:12.263Z","level":"info","stage":"test.stage","message":"Test message","data":{"foo":"bar"}}
@@ -10,7 +10,7 @@ import {
10
10
  generateTestCoverageSummary,
11
11
  scanTestFiles,
12
12
  truncateToTokenBudget,
13
- } from "../../src/context/test-scanner";
13
+ } from "../../../src/context/test-scanner";
14
14
 
15
15
  describe("extractTestStructure", () => {
16
16
  test("extracts describe and test blocks", () => {
@@ -1,10 +1,11 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for asset verification with contextFiles/expectedFiles split
3
4
  */
4
5
 
5
6
  import { describe, expect, test } from "bun:test";
6
- import { getExpectedFiles } from "../../src/prd";
7
- import type { UserStory } from "../../src/prd";
7
+ import { getExpectedFiles } from "../../../src/prd";
8
+ import type { UserStory } from "../../../src/prd";
8
9
 
9
10
  const createStory = (partial: Partial<UserStory>): UserStory => ({
10
11
  id: "US-001",
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for acceptance test generation module
3
4
  */
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for stderr capture in agent result type
3
4
  *
@@ -1,3 +1,4 @@
1
+ // RE-ARCH: keep
1
2
  /**
2
3
  * Tests for ClaudeCodeAdapter.runOnce() timeout behavior
3
4
  *
@@ -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
  */