@nathapp/nax 0.18.1
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/.gitlab-ci.yml +96 -0
- package/BRIEF.md +140 -0
- package/CHANGELOG.md +60 -0
- package/CLAUDE.md +159 -0
- package/README.md +373 -0
- package/US-007-IMPLEMENTATION.md +139 -0
- package/bin/nax.ts +930 -0
- package/biome.json +14 -0
- package/bun.lock +168 -0
- package/bunfig.toml +11 -0
- package/docs/20260216-fix-plan-context-review.md +56 -0
- package/docs/20260216-relentless-vs-ngent-comparison.md +208 -0
- package/docs/20260216-v02-plan.md +136 -0
- package/docs/20260216-v02-review.md +685 -0
- package/docs/20260217-dogfood-findings.md +56 -0
- package/docs/20260217-p2-plus-plan.md +117 -0
- package/docs/20260217-partial-fixes-plan.md +62 -0
- package/docs/20260217-plan-analyze-spec.md +117 -0
- package/docs/20260217-post-impl-review.md +1137 -0
- package/docs/20260217-quick-wins-plan.md +66 -0
- package/docs/20260217-split-runner-plan.md +75 -0
- package/docs/20260217-v03-impl-plan.md +80 -0
- package/docs/20260217-v03-post-impl-review.md +589 -0
- package/docs/20260217-v04-impl-plan.md +86 -0
- package/docs/20260217-v05-post-impl-review.md +850 -0
- package/docs/20260217-v06-post-impl-review.md +817 -0
- package/docs/20260218-adr003-port-plan.md +151 -0
- package/docs/20260218-review-adr003-verification.md +175 -0
- package/docs/20260219-fix-plan-bug16-19.md +79 -0
- package/docs/20260219-fix-plan-bug20-22.md +114 -0
- package/docs/20260219-plan-llm-routing.md +116 -0
- package/docs/20260219-review-bug20-22-fixes.md +135 -0
- package/docs/20260219-routing-baseline-keyword.md +63 -0
- package/docs/20260220-plan-structured-logging-p1.md +80 -0
- package/docs/20260220-plan-structured-logging-p2.md +37 -0
- package/docs/20260220-review-llm-routing.md +180 -0
- package/docs/20260220-review-post-fix-llm-routing.md +70 -0
- package/docs/20260221-fix-plan-relevantfiles-split.md +101 -0
- package/docs/20260221-fix-plan-routing-mode.md +125 -0
- package/docs/20260221-review-v0.9-implementation.md +379 -0
- package/docs/20260222-fix-plan-v091-routing-isolation.md +197 -0
- package/docs/20260223-fix-plan-prompt-audit.md +62 -0
- package/docs/20260224-nax-roadmap-phases.md +189 -0
- package/docs/20260225-phase2-llm-service-layer.md +401 -0
- package/docs/20260225-review-v0.10.1.md +187 -0
- package/docs/20260303-v010-implementation-plan.md +165 -0
- package/docs/CLAUDE.md.bak +191 -0
- package/docs/ROADMAP.md +165 -0
- package/docs/SPEC-rectification.md +0 -0
- package/docs/SPEC.md +324 -0
- package/docs/US-001-plugin-loading-verification.md +152 -0
- package/docs/architecture-analysis.md +1076 -0
- package/docs/bugs/BUG-21-escalation-null-attempts.md +48 -0
- package/docs/bugs-from-dogfood-run-c.md +243 -0
- package/docs/code-review-20260228.md +612 -0
- package/docs/code-review-v0.15.0.md +629 -0
- package/docs/hook-lifecycle-test-plan.md +149 -0
- package/docs/releases/v0.11.0-and-earlier.md +20 -0
- package/docs/releases/v0.12.0.md +15 -0
- package/docs/releases/v0.13.0.md +14 -0
- package/docs/releases/v0.14.0.md +20 -0
- package/docs/releases/v0.14.1.md +36 -0
- package/docs/releases/v0.14.2.md +51 -0
- package/docs/releases/v0.14.3.md +174 -0
- package/docs/releases/v0.14.4.md +94 -0
- package/docs/releases/v0.15.0.md +502 -0
- package/docs/releases/v0.15.1.md +170 -0
- package/docs/releases/v0.15.3.md +193 -0
- package/docs/specs/status-file-v0.10.1.md +812 -0
- package/docs/v0.10-global-config.md +206 -0
- package/docs/v0.10-plugin-system.md +415 -0
- package/docs/v0.10-prompt-optimizer.md +234 -0
- package/docs/v0.3-spec.md +244 -0
- package/docs/v0.4-spec.md +140 -0
- package/docs/v0.5-spec.md +237 -0
- package/docs/v0.6-spec.md +371 -0
- package/docs/v0.7-spec.md +177 -0
- package/docs/v0.8-llm-routing.md +206 -0
- package/docs/v0.8-structured-logging.md +132 -0
- package/docs/v0.9.3-prompt-audit.md +112 -0
- package/examples/plugins/console-reporter/index.test.ts +207 -0
- package/examples/plugins/console-reporter/index.ts +110 -0
- package/nax/config.json +147 -0
- package/nax/features/bugfix-v0171/prd.json +52 -0
- package/nax/features/config-management/prd.json +108 -0
- package/nax/features/config-management/progress.txt +5 -0
- package/nax/features/diagnose/acceptance.test.ts +412 -0
- package/nax/features/diagnose/prd.json +41 -0
- package/nax/features/orchestration-fixes/prd.json +89 -0
- package/nax/features/orchestration-fixes/progress.txt +1 -0
- package/nax/features/plugin-integration/US-007-VERIFICATION.md +259 -0
- package/nax/features/plugin-integration/prd.json +208 -0
- package/nax/features/plugin-integration/progress.txt +5 -0
- package/nax/features/precheck/prd.json +205 -0
- package/nax/features/precheck/progress.txt +15 -0
- package/nax/features/structured-logging/prd.json +199 -0
- package/nax/features/unlock/prd.json +36 -0
- package/package.json +47 -0
- package/src/acceptance/fix-generator.ts +348 -0
- package/src/acceptance/generator.ts +282 -0
- package/src/acceptance/index.ts +30 -0
- package/src/acceptance/types.ts +79 -0
- package/src/agents/claude-decompose.ts +169 -0
- package/src/agents/claude-plan.ts +139 -0
- package/src/agents/claude.ts +324 -0
- package/src/agents/cost.ts +268 -0
- package/src/agents/index.ts +13 -0
- package/src/agents/registry.ts +48 -0
- package/src/agents/types-extended.ts +133 -0
- package/src/agents/types.ts +113 -0
- package/src/agents/validation.ts +69 -0
- package/src/analyze/classifier.ts +305 -0
- package/src/analyze/index.ts +16 -0
- package/src/analyze/scanner.ts +175 -0
- package/src/analyze/types.ts +51 -0
- package/src/cli/accept.ts +108 -0
- package/src/cli/analyze-parser.ts +284 -0
- package/src/cli/analyze.ts +207 -0
- package/src/cli/config.ts +561 -0
- package/src/cli/constitution.ts +109 -0
- package/src/cli/diagnose-analysis.ts +159 -0
- package/src/cli/diagnose-formatter.ts +87 -0
- package/src/cli/diagnose.ts +203 -0
- package/src/cli/generate.ts +127 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/init.ts +188 -0
- package/src/cli/interact.ts +295 -0
- package/src/cli/plan.ts +198 -0
- package/src/cli/plugins.ts +111 -0
- package/src/cli/prompts.ts +295 -0
- package/src/cli/runs.ts +174 -0
- package/src/cli/status-cost.ts +151 -0
- package/src/cli/status-features.ts +338 -0
- package/src/cli/status.ts +13 -0
- package/src/commands/common.ts +171 -0
- package/src/commands/diagnose.ts +17 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/logs.ts +384 -0
- package/src/commands/precheck.ts +86 -0
- package/src/commands/unlock.ts +96 -0
- package/src/config/defaults.ts +160 -0
- package/src/config/index.ts +22 -0
- package/src/config/loader.ts +121 -0
- package/src/config/merger.ts +147 -0
- package/src/config/path-security.ts +121 -0
- package/src/config/paths.ts +27 -0
- package/src/config/schema.ts +56 -0
- package/src/config/schemas.ts +286 -0
- package/src/config/types.ts +423 -0
- package/src/config/validate.ts +103 -0
- package/src/constitution/generator.ts +191 -0
- package/src/constitution/generators/aider.ts +41 -0
- package/src/constitution/generators/claude.ts +35 -0
- package/src/constitution/generators/cursor.ts +36 -0
- package/src/constitution/generators/opencode.ts +38 -0
- package/src/constitution/generators/types.ts +33 -0
- package/src/constitution/generators/windsurf.ts +36 -0
- package/src/constitution/index.ts +10 -0
- package/src/constitution/loader.ts +133 -0
- package/src/constitution/types.ts +31 -0
- package/src/context/auto-detect.ts +227 -0
- package/src/context/builder.ts +246 -0
- package/src/context/elements.ts +83 -0
- package/src/context/formatter.ts +107 -0
- package/src/context/generator.ts +129 -0
- package/src/context/generators/aider.ts +34 -0
- package/src/context/generators/claude.ts +28 -0
- package/src/context/generators/cursor.ts +28 -0
- package/src/context/generators/opencode.ts +30 -0
- package/src/context/generators/windsurf.ts +28 -0
- package/src/context/greenfield.ts +114 -0
- package/src/context/index.ts +33 -0
- package/src/context/injector.ts +279 -0
- package/src/context/test-scanner.ts +370 -0
- package/src/context/types.ts +98 -0
- package/src/errors.ts +67 -0
- package/src/execution/batching.ts +157 -0
- package/src/execution/crash-recovery.ts +373 -0
- package/src/execution/escalation/escalation.ts +44 -0
- package/src/execution/escalation/index.ts +13 -0
- package/src/execution/escalation/tier-escalation.ts +295 -0
- package/src/execution/escalation/tier-outcome.ts +158 -0
- package/src/execution/helpers.ts +38 -0
- package/src/execution/index.ts +45 -0
- package/src/execution/lifecycle/acceptance-loop.ts +272 -0
- package/src/execution/lifecycle/headless-formatter.ts +85 -0
- package/src/execution/lifecycle/index.ts +12 -0
- package/src/execution/lifecycle/parallel-lifecycle.ts +101 -0
- package/src/execution/lifecycle/precheck-runner.ts +140 -0
- package/src/execution/lifecycle/run-cleanup.ts +81 -0
- package/src/execution/lifecycle/run-completion.ts +129 -0
- package/src/execution/lifecycle/run-initialization.ts +141 -0
- package/src/execution/lifecycle/run-lifecycle.ts +312 -0
- package/src/execution/lifecycle/run-setup.ts +204 -0
- package/src/execution/lifecycle/story-hooks.ts +38 -0
- package/src/execution/lifecycle/story-size-prompts.ts +123 -0
- package/src/execution/lock.ts +115 -0
- package/src/execution/parallel-executor.ts +216 -0
- package/src/execution/parallel.ts +400 -0
- package/src/execution/pid-registry.ts +280 -0
- package/src/execution/pipeline-result-handler.ts +388 -0
- package/src/execution/post-verify-rectification.ts +188 -0
- package/src/execution/post-verify.ts +274 -0
- package/src/execution/progress.ts +25 -0
- package/src/execution/prompts.ts +127 -0
- package/src/execution/queue-handler.ts +109 -0
- package/src/execution/rectification.ts +13 -0
- package/src/execution/runner.ts +377 -0
- package/src/execution/sequential-executor.ts +388 -0
- package/src/execution/status-file.ts +264 -0
- package/src/execution/status-writer.ts +139 -0
- package/src/execution/story-context.ts +229 -0
- package/src/execution/test-output-parser.ts +14 -0
- package/src/execution/verification.ts +72 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/runner.ts +286 -0
- package/src/hooks/types.ts +67 -0
- package/src/interaction/chain.ts +154 -0
- package/src/interaction/index.ts +60 -0
- package/src/interaction/init.ts +83 -0
- package/src/interaction/plugins/auto.ts +217 -0
- package/src/interaction/plugins/cli.ts +300 -0
- package/src/interaction/plugins/telegram.ts +384 -0
- package/src/interaction/plugins/webhook.ts +258 -0
- package/src/interaction/state.ts +171 -0
- package/src/interaction/triggers.ts +229 -0
- package/src/interaction/types.ts +163 -0
- package/src/logger/formatters.ts +84 -0
- package/src/logger/index.ts +16 -0
- package/src/logger/logger.ts +298 -0
- package/src/logger/types.ts +48 -0
- package/src/logging/formatter.ts +355 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/types.ts +93 -0
- package/src/metrics/aggregator.ts +190 -0
- package/src/metrics/index.ts +14 -0
- package/src/metrics/tracker.ts +200 -0
- package/src/metrics/types.ts +109 -0
- package/src/optimizer/index.ts +62 -0
- package/src/optimizer/noop.optimizer.ts +24 -0
- package/src/optimizer/rule-based.optimizer.ts +248 -0
- package/src/optimizer/types.ts +53 -0
- package/src/pipeline/events.ts +130 -0
- package/src/pipeline/index.ts +19 -0
- package/src/pipeline/runner.ts +161 -0
- package/src/pipeline/stages/acceptance.ts +197 -0
- package/src/pipeline/stages/completion.ts +99 -0
- package/src/pipeline/stages/constitution.ts +63 -0
- package/src/pipeline/stages/context.ts +117 -0
- package/src/pipeline/stages/execution.ts +194 -0
- package/src/pipeline/stages/index.ts +62 -0
- package/src/pipeline/stages/optimizer.ts +74 -0
- package/src/pipeline/stages/prompt.ts +57 -0
- package/src/pipeline/stages/queue-check.ts +103 -0
- package/src/pipeline/stages/review.ts +181 -0
- package/src/pipeline/stages/routing.ts +81 -0
- package/src/pipeline/stages/verify.ts +100 -0
- package/src/pipeline/types.ts +167 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/loader.ts +287 -0
- package/src/plugins/registry.ts +168 -0
- package/src/plugins/types.ts +327 -0
- package/src/plugins/validator.ts +352 -0
- package/src/prd/index.ts +172 -0
- package/src/prd/types.ts +202 -0
- package/src/precheck/checks-blockers.ts +391 -0
- package/src/precheck/checks-warnings.ts +142 -0
- package/src/precheck/checks.ts +30 -0
- package/src/precheck/index.ts +247 -0
- package/src/precheck/story-size-gate.ts +144 -0
- package/src/precheck/types.ts +31 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/manager.ts +254 -0
- package/src/queue/types.ts +54 -0
- package/src/review/index.ts +8 -0
- package/src/review/runner.ts +172 -0
- package/src/review/types.ts +66 -0
- package/src/routing/builder.ts +81 -0
- package/src/routing/chain.ts +74 -0
- package/src/routing/index.ts +16 -0
- package/src/routing/loader.ts +58 -0
- package/src/routing/router.ts +303 -0
- package/src/routing/strategies/adaptive.ts +215 -0
- package/src/routing/strategies/index.ts +8 -0
- package/src/routing/strategies/keyword.ts +163 -0
- package/src/routing/strategies/llm-prompts.ts +209 -0
- package/src/routing/strategies/llm.ts +235 -0
- package/src/routing/strategies/manual.ts +50 -0
- package/src/routing/strategy.ts +99 -0
- package/src/tdd/cleanup.ts +111 -0
- package/src/tdd/index.ts +23 -0
- package/src/tdd/isolation.ts +123 -0
- package/src/tdd/orchestrator.ts +383 -0
- package/src/tdd/prompts.ts +270 -0
- package/src/tdd/rectification-gate.ts +183 -0
- package/src/tdd/session-runner.ts +179 -0
- package/src/tdd/types.ts +81 -0
- package/src/tdd/verdict.ts +271 -0
- package/src/tui/App.tsx +265 -0
- package/src/tui/components/AgentPanel.tsx +75 -0
- package/src/tui/components/CostOverlay.tsx +118 -0
- package/src/tui/components/HelpOverlay.tsx +107 -0
- package/src/tui/components/StatusBar.tsx +63 -0
- package/src/tui/components/StoriesPanel.tsx +177 -0
- package/src/tui/hooks/useKeyboard.ts +142 -0
- package/src/tui/hooks/useLayout.ts +137 -0
- package/src/tui/hooks/usePipelineEvents.ts +183 -0
- package/src/tui/hooks/usePty.ts +194 -0
- package/src/tui/index.tsx +38 -0
- package/src/tui/types.ts +76 -0
- package/src/utils/git.ts +83 -0
- package/src/utils/queue-writer.ts +54 -0
- package/src/verification/executor.ts +235 -0
- package/src/verification/gate.ts +207 -0
- package/src/verification/index.ts +12 -0
- package/src/verification/parser.ts +230 -0
- package/src/verification/rectification.ts +108 -0
- package/src/verification/types.ts +113 -0
- package/src/worktree/dispatcher.ts +65 -0
- package/src/worktree/index.ts +2 -0
- package/src/worktree/manager.ts +187 -0
- package/src/worktree/merge.ts +301 -0
- package/src/worktree/types.ts +4 -0
- package/test/TEST_COVERAGE_US001.md +217 -0
- package/test/TEST_COVERAGE_US003.md +84 -0
- package/test/TEST_COVERAGE_US005.md +86 -0
- package/test/US-002-orchestrator.test.ts +246 -0
- package/test/acceptance/cm-003-default-view.test.ts +194 -0
- package/test/execution/pid-registry.test.ts +240 -0
- package/test/execution/post-verify.test.ts +224 -0
- package/test/helpers/timeout.ts +42 -0
- package/test/integration/US-002-TEST-SUMMARY.md +107 -0
- package/test/integration/US-003-TEST-SUMMARY.md +149 -0
- package/test/integration/US-004-TEST-SUMMARY.md +106 -0
- package/test/integration/US-005-TEST-SUMMARY.md +138 -0
- package/test/integration/US-007-TEST-SUMMARY.md +100 -0
- package/test/integration/agent-validation.test.ts +439 -0
- package/test/integration/analyze-integration.test.ts +261 -0
- package/test/integration/analyze-scanner.test.ts +131 -0
- package/test/integration/cli-config-default-edge-cases.test.ts +222 -0
- package/test/integration/cli-config-default-view.test.ts +229 -0
- package/test/integration/cli-config-diff.test.ts +460 -0
- package/test/integration/cli-config.test.ts +736 -0
- package/test/integration/cli-diagnose.test.ts +592 -0
- package/test/integration/cli-logs.test.ts +314 -0
- package/test/integration/cli-plugins.test.ts +678 -0
- package/test/integration/cli-precheck.test.ts +371 -0
- package/test/integration/cli-run-headless.test.ts +173 -0
- package/test/integration/cli.test.ts +75 -0
- package/test/integration/config/merger.test.ts +465 -0
- package/test/integration/config/paths.test.ts +51 -0
- package/test/integration/config-loader.test.ts +265 -0
- package/test/integration/config.test.ts +444 -0
- package/test/integration/context-integration.test.ts +702 -0
- package/test/integration/context-provider-injection.test.ts +506 -0
- package/test/integration/context-verification-integration.test.ts +295 -0
- package/test/integration/e2e.test.ts +896 -0
- package/test/integration/execution.test.ts +625 -0
- package/test/integration/helpers.test.ts +295 -0
- package/test/integration/hooks.test.ts +361 -0
- package/test/integration/interaction-chain-pipeline.test.ts +464 -0
- package/test/integration/isolation.test.ts +143 -0
- package/test/integration/logger.test.ts +461 -0
- package/test/integration/parallel.test.ts +250 -0
- package/test/integration/path-security.test.ts +173 -0
- package/test/integration/pipeline-acceptance.test.ts +302 -0
- package/test/integration/pipeline-events.test.ts +475 -0
- package/test/integration/pipeline.test.ts +658 -0
- package/test/integration/plan.test.ts +157 -0
- package/test/integration/plugin-routing.test.ts +921 -0
- package/test/integration/plugins/config-integration.test.ts +172 -0
- package/test/integration/plugins/config-resolution.test.ts +522 -0
- package/test/integration/plugins/loader.test.ts +641 -0
- package/test/integration/plugins/registry.test.ts +746 -0
- package/test/integration/plugins/validator.test.ts +563 -0
- package/test/integration/prd-pause.test.ts +205 -0
- package/test/integration/prd-resolvers.test.ts +185 -0
- package/test/integration/precheck-integration.test.ts +468 -0
- package/test/integration/precheck.test.ts +805 -0
- package/test/integration/progress.test.ts +34 -0
- package/test/integration/rectification-flow.test.ts +512 -0
- package/test/integration/reporter-lifecycle.test.ts +860 -0
- package/test/integration/review-config-commands.test.ts +319 -0
- package/test/integration/review-config-schema.test.ts +116 -0
- package/test/integration/review-plugin-integration.test.ts +722 -0
- package/test/integration/review.test.ts +149 -0
- package/test/integration/routing-stage-bug-021.test.ts +274 -0
- package/test/integration/routing-stage-greenfield.test.ts +286 -0
- package/test/integration/runner-config-plugins.test.ts +461 -0
- package/test/integration/runner-fixes.test.ts +399 -0
- package/test/integration/runner-plugin-integration.test.ts +543 -0
- package/test/integration/runner.test.ts +1679 -0
- package/test/integration/s5-greenfield-fallback.test.ts +297 -0
- package/test/integration/status-file-integration.test.ts +325 -0
- package/test/integration/status-file.test.ts +379 -0
- package/test/integration/status-writer.test.ts +345 -0
- package/test/integration/story-id-in-events.test.ts +273 -0
- package/test/integration/tdd-cleanup.test.ts +246 -0
- package/test/integration/tdd-orchestrator.test.ts +1762 -0
- package/test/integration/test-scanner.test.ts +403 -0
- package/test/integration/verification-asset-check.test.ts +142 -0
- package/test/integration/verify-stage.test.ts +275 -0
- package/test/integration/worktree/manager.test.ts +218 -0
- package/test/integration/worktree/merge.test.ts +341 -0
- package/test/manual/logging-formatter-demo.ts +158 -0
- package/test/ui/tui-agent-panel.test.tsx +99 -0
- package/test/ui/tui-controls.test.ts +334 -0
- package/test/ui/tui-cost-and-pty.test.ts +189 -0
- package/test/ui/tui-layout.test.ts +378 -0
- package/test/ui/tui-pty-integration.test.tsx +159 -0
- package/test/ui/tui-stories.test.ts +332 -0
- package/test/unit/acceptance.test.ts +186 -0
- package/test/unit/agent-stderr-capture.test.ts +146 -0
- package/test/unit/analyze-classifier.test.ts +215 -0
- package/test/unit/analyze.test.ts +224 -0
- package/test/unit/auto-detect.test.ts +249 -0
- package/test/unit/cli-status.test.ts +417 -0
- package/test/unit/commands/common.test.ts +320 -0
- package/test/unit/commands/logs.test.ts +416 -0
- package/test/unit/commands/unlock.test.ts +319 -0
- package/test/unit/constitution-generators.test.ts +160 -0
- package/test/unit/constitution.test.ts +209 -0
- package/test/unit/context.test.ts +1722 -0
- package/test/unit/cost.test.ts +231 -0
- package/test/unit/crash-recovery.test.ts +308 -0
- package/test/unit/escalation.test.ts +126 -0
- package/test/unit/execution-logging-stderr.test.ts +156 -0
- package/test/unit/execution-stage.test.ts +122 -0
- package/test/unit/fix-generator.test.ts +275 -0
- package/test/unit/formatters.test.ts +469 -0
- package/test/unit/greenfield.test.ts +179 -0
- package/test/unit/helpers.test.ts +317 -0
- package/test/unit/interaction/human-review-trigger.test.ts +164 -0
- package/test/unit/interaction-network-failures.test.ts +389 -0
- package/test/unit/interaction-plugins.test.ts +164 -0
- package/test/unit/isolation.test.ts +134 -0
- package/test/unit/logging/formatter.test.ts +455 -0
- package/test/unit/merge.test.ts +268 -0
- package/test/unit/metrics.test.ts +276 -0
- package/test/unit/optimizer/noop.optimizer.test.ts +125 -0
- package/test/unit/optimizer/rule-based.optimizer.test.ts +358 -0
- package/test/unit/prd-auto-default.test.ts +290 -0
- package/test/unit/prd-failure-category.test.ts +176 -0
- package/test/unit/prd-get-next-story.test.ts +186 -0
- package/test/unit/precheck-checks.test.ts +840 -0
- package/test/unit/precheck-story-size-gate.test.ts +287 -0
- package/test/unit/precheck-types.test.ts +142 -0
- package/test/unit/prompts.test.ts +475 -0
- package/test/unit/queue.test.ts +237 -0
- package/test/unit/rectification.test.ts +284 -0
- package/test/unit/registry.test.ts +287 -0
- package/test/unit/routing.test.ts +937 -0
- package/test/unit/run-lifecycle.test.ts +140 -0
- package/test/unit/storyid-events.test.ts +224 -0
- package/test/unit/tdd-verdict.test.ts +492 -0
- package/test/unit/test-output-parser.test.ts +377 -0
- package/test/unit/verdict.test.ts +324 -0
- package/test/unit/worktree-manager.test.ts +158 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-002: Precheck orchestrator with formatted output
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verification tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
// Skip in CI: AC2, AC5, AC6 call runPrecheck() which includes checkClaudeCLI as a
|
|
10
|
+
// Tier 1 blocker. Without the claude binary installed, blockers.length > 0 always,
|
|
11
|
+
// breaking assertions like expect(blockers.length).toBe(0). These ACs test correct
|
|
12
|
+
// orchestration behaviour and pass reliably on Mac01/VPS where claude is installed.
|
|
13
|
+
const skipInCI = process.env.CI ? test.skip : test;
|
|
14
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import type { NaxConfig } from "../src/config";
|
|
18
|
+
import type { PRD } from "../src/prd/types";
|
|
19
|
+
import { EXIT_CODES, runPrecheck } from "../src/precheck";
|
|
20
|
+
|
|
21
|
+
// Helper to create a minimal valid git environment
|
|
22
|
+
async function setupGitRepo(dir: string): Promise<void> {
|
|
23
|
+
mkdirSync(join(dir, ".git"));
|
|
24
|
+
await Bun.spawn(["git", "init"], { cwd: dir, stdout: "ignore", stderr: "ignore" }).exited;
|
|
25
|
+
await Bun.spawn(["git", "config", "user.name", "Test"], { cwd: dir, stdout: "ignore", stderr: "ignore" }).exited;
|
|
26
|
+
await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: dir, stdout: "ignore", stderr: "ignore" })
|
|
27
|
+
.exited;
|
|
28
|
+
writeFileSync(join(dir, "README.md"), "# Test");
|
|
29
|
+
await Bun.spawn(["git", "add", "."], { cwd: dir, stdout: "ignore", stderr: "ignore" }).exited;
|
|
30
|
+
await Bun.spawn(["git", "commit", "-m", "init"], { cwd: dir, stdout: "ignore", stderr: "ignore" }).exited;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const createConfig = (workdir: string): NaxConfig =>
|
|
34
|
+
({
|
|
35
|
+
execution: {
|
|
36
|
+
maxIterations: 10,
|
|
37
|
+
iterationDelayMs: 1000,
|
|
38
|
+
maxCostUSD: 10,
|
|
39
|
+
testCommand: "echo test",
|
|
40
|
+
lintCommand: "echo lint",
|
|
41
|
+
typecheckCommand: "echo typecheck",
|
|
42
|
+
contextProviderTokenBudget: 2000,
|
|
43
|
+
requireExplicitContextFiles: false,
|
|
44
|
+
preflightExpectedFilesEnabled: false,
|
|
45
|
+
cwd: workdir,
|
|
46
|
+
},
|
|
47
|
+
autoMode: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
defaultAgent: "test",
|
|
50
|
+
fallbackOrder: [],
|
|
51
|
+
complexityRouting: {},
|
|
52
|
+
escalation: { enabled: false, tierOrder: [] },
|
|
53
|
+
},
|
|
54
|
+
quality: { minTestCoverage: 80, requireTypecheck: true, requireLint: true },
|
|
55
|
+
tdd: { strategy: "auto", skipGeneratedVerificationTests: false },
|
|
56
|
+
models: {},
|
|
57
|
+
rectification: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
maxRetries: 2,
|
|
60
|
+
fullSuiteTimeoutSeconds: 120,
|
|
61
|
+
maxFailureSummaryChars: 2000,
|
|
62
|
+
abortOnIncreasingFailures: true,
|
|
63
|
+
},
|
|
64
|
+
}) as NaxConfig;
|
|
65
|
+
|
|
66
|
+
const createPRD = (): PRD => ({
|
|
67
|
+
project: "test",
|
|
68
|
+
feature: "test-feature",
|
|
69
|
+
branchName: "test",
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
updatedAt: new Date().toISOString(),
|
|
72
|
+
userStories: [
|
|
73
|
+
{
|
|
74
|
+
id: "US-001",
|
|
75
|
+
title: "Test",
|
|
76
|
+
description: "Test description",
|
|
77
|
+
acceptanceCriteria: [],
|
|
78
|
+
tags: [],
|
|
79
|
+
dependencies: [],
|
|
80
|
+
status: "pending",
|
|
81
|
+
passes: false,
|
|
82
|
+
escalations: [],
|
|
83
|
+
attempts: 0,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("US-002: Precheck orchestrator acceptance criteria", () => {
|
|
89
|
+
let testDir: string;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
testDir = mkdtempSync(join(tmpdir(), "nax-test-us002-"));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("AC1: Runs Tier 1 checks first, stops on first failure", async () => {
|
|
100
|
+
// No .git directory - should fail on first check
|
|
101
|
+
const config = createConfig(testDir);
|
|
102
|
+
const prd = createPRD();
|
|
103
|
+
|
|
104
|
+
const { result } = await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
105
|
+
|
|
106
|
+
// Should have exactly 1 blocker (fail-fast)
|
|
107
|
+
expect(result.blockers.length).toBe(1);
|
|
108
|
+
expect(result.blockers[0].name).toBe("git-repo-exists");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
skipInCI("AC2: Runs all Tier 2 checks even if some warn", async () => {
|
|
112
|
+
// Create valid Tier 1 environment
|
|
113
|
+
await setupGitRepo(testDir);
|
|
114
|
+
mkdirSync(join(testDir, "node_modules"));
|
|
115
|
+
|
|
116
|
+
const config = createConfig(testDir);
|
|
117
|
+
const prd = createPRD();
|
|
118
|
+
|
|
119
|
+
const { result } = await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
120
|
+
|
|
121
|
+
// No blockers
|
|
122
|
+
expect(result.blockers.length).toBe(0);
|
|
123
|
+
|
|
124
|
+
// All Tier 2 checks should run (5 total)
|
|
125
|
+
// At least 2 will fail (CLAUDE.md, .gitignore)
|
|
126
|
+
expect(result.warnings.length).toBeGreaterThanOrEqual(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("AC3: Human output shows emoji per check result", async () => {
|
|
130
|
+
await setupGitRepo(testDir);
|
|
131
|
+
mkdirSync(join(testDir, "node_modules"));
|
|
132
|
+
|
|
133
|
+
const config = createConfig(testDir);
|
|
134
|
+
const prd = createPRD();
|
|
135
|
+
|
|
136
|
+
// Capture console output
|
|
137
|
+
const originalLog = console.log;
|
|
138
|
+
const logs: string[] = [];
|
|
139
|
+
console.log = (msg: string) => logs.push(msg);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await runPrecheck(config, prd, { workdir: testDir, format: "human" });
|
|
143
|
+
|
|
144
|
+
// Should have emoji indicators
|
|
145
|
+
const hasCheckmark = logs.some((l) => l.includes("✓"));
|
|
146
|
+
const hasWarning = logs.some((l) => l.includes("⚠"));
|
|
147
|
+
|
|
148
|
+
expect(hasCheckmark || hasWarning).toBe(true);
|
|
149
|
+
} finally {
|
|
150
|
+
console.log = originalLog;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("AC4: JSON output matches spec schema", async () => {
|
|
155
|
+
await setupGitRepo(testDir);
|
|
156
|
+
mkdirSync(join(testDir, "node_modules"));
|
|
157
|
+
|
|
158
|
+
const config = createConfig(testDir);
|
|
159
|
+
const prd = createPRD();
|
|
160
|
+
|
|
161
|
+
const originalLog = console.log;
|
|
162
|
+
let jsonOutput = "";
|
|
163
|
+
console.log = (msg: string) => {
|
|
164
|
+
jsonOutput += msg;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
169
|
+
|
|
170
|
+
const output = JSON.parse(jsonOutput);
|
|
171
|
+
|
|
172
|
+
// Verify schema fields
|
|
173
|
+
expect(typeof output.passed).toBe("boolean");
|
|
174
|
+
expect(Array.isArray(output.blockers)).toBe(true);
|
|
175
|
+
expect(Array.isArray(output.warnings)).toBe(true);
|
|
176
|
+
expect(output.summary).toBeDefined();
|
|
177
|
+
expect(typeof output.summary.total).toBe("number");
|
|
178
|
+
expect(typeof output.summary.passed).toBe("number");
|
|
179
|
+
expect(typeof output.summary.failed).toBe("number");
|
|
180
|
+
expect(typeof output.summary.warnings).toBe("number");
|
|
181
|
+
expect(output.feature).toBe("test-feature");
|
|
182
|
+
} finally {
|
|
183
|
+
console.log = originalLog;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
skipInCI("AC5: Exit code 0 for pass, 1 for blocker, 2 for invalid PRD", async () => {
|
|
188
|
+
// Test exit code 0 (pass)
|
|
189
|
+
await setupGitRepo(testDir);
|
|
190
|
+
mkdirSync(join(testDir, "node_modules"));
|
|
191
|
+
writeFileSync(join(testDir, "CLAUDE.md"), "# Test");
|
|
192
|
+
writeFileSync(join(testDir, ".gitignore"), "nax.lock\nruns/\ntest/tmp/");
|
|
193
|
+
await Bun.spawn(["git", "add", "."], { cwd: testDir, stdout: "ignore" }).exited;
|
|
194
|
+
await Bun.spawn(["git", "commit", "-m", "add"], { cwd: testDir, stdout: "ignore" }).exited;
|
|
195
|
+
|
|
196
|
+
let config = createConfig(testDir);
|
|
197
|
+
const prd = createPRD();
|
|
198
|
+
let result = await runPrecheck(config, prd, { workdir: testDir, format: "json" });
|
|
199
|
+
expect(result.exitCode).toBe(EXIT_CODES.SUCCESS);
|
|
200
|
+
|
|
201
|
+
// Test exit code 1 (blocker)
|
|
202
|
+
const testDir2 = mkdtempSync(join(tmpdir(), "nax-test-blocker-"));
|
|
203
|
+
config = createConfig(testDir2);
|
|
204
|
+
result = await runPrecheck(config, prd, { workdir: testDir2, format: "json" });
|
|
205
|
+
expect(result.exitCode).toBe(EXIT_CODES.BLOCKER);
|
|
206
|
+
rmSync(testDir2, { recursive: true, force: true });
|
|
207
|
+
|
|
208
|
+
// Test exit code 2 (invalid PRD)
|
|
209
|
+
const testDir3 = mkdtempSync(join(tmpdir(), "nax-test-invalid-prd-"));
|
|
210
|
+
await setupGitRepo(testDir3);
|
|
211
|
+
mkdirSync(join(testDir3, "node_modules"));
|
|
212
|
+
config = createConfig(testDir3);
|
|
213
|
+
const invalidPRD: PRD = {
|
|
214
|
+
...prd,
|
|
215
|
+
userStories: [{ ...prd.userStories[0], id: "", title: "", description: "" }],
|
|
216
|
+
};
|
|
217
|
+
result = await runPrecheck(config, invalidPRD, { workdir: testDir3, format: "json" });
|
|
218
|
+
expect(result.exitCode).toBe(EXIT_CODES.INVALID_PRD);
|
|
219
|
+
rmSync(testDir3, { recursive: true, force: true });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("AC6: Summary line shows total checks/passed/failed/warnings", async () => {
|
|
223
|
+
await setupGitRepo(testDir);
|
|
224
|
+
mkdirSync(join(testDir, "node_modules"));
|
|
225
|
+
|
|
226
|
+
const config = createConfig(testDir);
|
|
227
|
+
const prd = createPRD();
|
|
228
|
+
|
|
229
|
+
const originalLog = console.log;
|
|
230
|
+
const logs: string[] = [];
|
|
231
|
+
console.log = (msg: string) => logs.push(msg);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await runPrecheck(config, prd, { workdir: testDir, format: "human" });
|
|
235
|
+
|
|
236
|
+
// Should have summary with counts
|
|
237
|
+
const hasSummary = logs.some(
|
|
238
|
+
(l) => l.includes("total") && l.includes("passed") && (l.includes("failed") || l.includes("warnings")),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(hasSummary).toBe(true);
|
|
242
|
+
} finally {
|
|
243
|
+
console.log = originalLog;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance Tests for CM-003: nax config (default view)
|
|
3
|
+
*
|
|
4
|
+
* Tests the acceptance criteria for running `nax config` without flags.
|
|
5
|
+
*
|
|
6
|
+
* ACCEPTANCE CRITERIA:
|
|
7
|
+
* 1. Running `nax config` prints the effective merged config as formatted JSON
|
|
8
|
+
* 2. Header shows paths of config files found (global, project)
|
|
9
|
+
* 3. Missing config files noted in header
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
13
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
describe("CM-003: nax config (default view)", () => {
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
let originalCwd: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = mkdtempSync(join(tmpdir(), "cm-003-test-"));
|
|
23
|
+
originalCwd = process.cwd();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
process.chdir(originalCwd);
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// AC1: Running `nax config` prints the effective merged config as formatted JSON
|
|
32
|
+
test("AC1: prints effective merged config as formatted JSON", async () => {
|
|
33
|
+
process.chdir(tempDir);
|
|
34
|
+
|
|
35
|
+
const proc = Bun.spawn(["bun", join(import.meta.dir, "../../bin/nax.ts"), "config"], {
|
|
36
|
+
cwd: tempDir,
|
|
37
|
+
stdout: "pipe",
|
|
38
|
+
stderr: "pipe",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const output = await new Response(proc.stdout).text();
|
|
42
|
+
const exitCode = await proc.exited;
|
|
43
|
+
|
|
44
|
+
expect(exitCode).toBe(0);
|
|
45
|
+
|
|
46
|
+
// Should output valid JSON
|
|
47
|
+
const lines = output.split("\n");
|
|
48
|
+
const jsonStartIndex = lines.findIndex((line) => line.startsWith("{"));
|
|
49
|
+
expect(jsonStartIndex).toBeGreaterThanOrEqual(0);
|
|
50
|
+
|
|
51
|
+
const jsonOutput = lines.slice(jsonStartIndex).join("\n");
|
|
52
|
+
expect(() => JSON.parse(jsonOutput)).not.toThrow();
|
|
53
|
+
|
|
54
|
+
// Verify it's the merged config (contains version, models, etc.)
|
|
55
|
+
const parsed = JSON.parse(jsonOutput);
|
|
56
|
+
expect(parsed.version).toBe(1);
|
|
57
|
+
expect(parsed.models).toBeDefined();
|
|
58
|
+
expect(parsed.autoMode).toBeDefined();
|
|
59
|
+
expect(parsed.execution).toBeDefined();
|
|
60
|
+
|
|
61
|
+
// Verify it's formatted (pretty-printed with indentation)
|
|
62
|
+
expect(jsonOutput).toContain(" "); // Should have 2-space indentation
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// AC2: Header shows paths of config files found (global, project)
|
|
66
|
+
test("AC2: header shows global config path", async () => {
|
|
67
|
+
process.chdir(tempDir);
|
|
68
|
+
|
|
69
|
+
const proc = Bun.spawn(["bun", join(import.meta.dir, "../../bin/nax.ts"), "config"], {
|
|
70
|
+
cwd: tempDir,
|
|
71
|
+
stdout: "pipe",
|
|
72
|
+
stderr: "pipe",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const output = await new Response(proc.stdout).text();
|
|
76
|
+
const exitCode = await proc.exited;
|
|
77
|
+
|
|
78
|
+
expect(exitCode).toBe(0);
|
|
79
|
+
|
|
80
|
+
// Should show global config line
|
|
81
|
+
expect(output).toContain("// Global config:");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("AC2: header shows project config path when present", async () => {
|
|
85
|
+
// Create project config
|
|
86
|
+
const naxDir = join(tempDir, "nax");
|
|
87
|
+
mkdirSync(naxDir, { recursive: true });
|
|
88
|
+
writeFileSync(
|
|
89
|
+
join(naxDir, "config.json"),
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
execution: {
|
|
92
|
+
maxIterations: 42,
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
process.chdir(tempDir);
|
|
98
|
+
|
|
99
|
+
const proc = Bun.spawn(["bun", join(import.meta.dir, "../../bin/nax.ts"), "config"], {
|
|
100
|
+
cwd: tempDir,
|
|
101
|
+
stdout: "pipe",
|
|
102
|
+
stderr: "pipe",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const output = await new Response(proc.stdout).text();
|
|
106
|
+
const exitCode = await proc.exited;
|
|
107
|
+
|
|
108
|
+
expect(exitCode).toBe(0);
|
|
109
|
+
|
|
110
|
+
// Should show project config path
|
|
111
|
+
expect(output).toContain("// Project config:");
|
|
112
|
+
expect(output).toContain("config.json");
|
|
113
|
+
|
|
114
|
+
// Verify the merged config reflects project override
|
|
115
|
+
const lines = output.split("\n");
|
|
116
|
+
const jsonStartIndex = lines.findIndex((line) => line.startsWith("{"));
|
|
117
|
+
const jsonOutput = lines.slice(jsonStartIndex).join("\n");
|
|
118
|
+
const parsed = JSON.parse(jsonOutput);
|
|
119
|
+
expect(parsed.execution.maxIterations).toBe(42);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// AC3: Missing config files noted in header
|
|
123
|
+
test("AC3: notes missing project config in header", async () => {
|
|
124
|
+
// Use a directory without nax/config.json
|
|
125
|
+
const isolatedDir = join(tempDir, "isolated");
|
|
126
|
+
mkdirSync(isolatedDir, { recursive: true });
|
|
127
|
+
process.chdir(isolatedDir);
|
|
128
|
+
|
|
129
|
+
const proc = Bun.spawn(["bun", join(import.meta.dir, "../../bin/nax.ts"), "config"], {
|
|
130
|
+
cwd: isolatedDir,
|
|
131
|
+
stdout: "pipe",
|
|
132
|
+
stderr: "pipe",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const output = await new Response(proc.stdout).text();
|
|
136
|
+
const exitCode = await proc.exited;
|
|
137
|
+
|
|
138
|
+
expect(exitCode).toBe(0);
|
|
139
|
+
|
|
140
|
+
// Should indicate project config is not found
|
|
141
|
+
expect(output).toContain("// Project config:");
|
|
142
|
+
expect(output).toContain("(not found)");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Additional verification: header includes resolution order info
|
|
146
|
+
test("header includes resolution order information", async () => {
|
|
147
|
+
process.chdir(tempDir);
|
|
148
|
+
|
|
149
|
+
const proc = Bun.spawn(["bun", join(import.meta.dir, "../../bin/nax.ts"), "config"], {
|
|
150
|
+
cwd: tempDir,
|
|
151
|
+
stdout: "pipe",
|
|
152
|
+
stderr: "pipe",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const output = await new Response(proc.stdout).text();
|
|
156
|
+
const exitCode = await proc.exited;
|
|
157
|
+
|
|
158
|
+
expect(exitCode).toBe(0);
|
|
159
|
+
|
|
160
|
+
// Should show resolution order
|
|
161
|
+
expect(output).toContain("// Resolution order:");
|
|
162
|
+
expect(output).toContain("defaults");
|
|
163
|
+
expect(output).toContain("global");
|
|
164
|
+
expect(output).toContain("project");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Additional verification: header precedes JSON
|
|
168
|
+
test("header appears before JSON output with blank line separator", async () => {
|
|
169
|
+
process.chdir(tempDir);
|
|
170
|
+
|
|
171
|
+
const proc = Bun.spawn(["bun", join(import.meta.dir, "../../bin/nax.ts"), "config"], {
|
|
172
|
+
cwd: tempDir,
|
|
173
|
+
stdout: "pipe",
|
|
174
|
+
stderr: "pipe",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const output = await new Response(proc.stdout).text();
|
|
178
|
+
const exitCode = await proc.exited;
|
|
179
|
+
|
|
180
|
+
expect(exitCode).toBe(0);
|
|
181
|
+
|
|
182
|
+
const lines = output.split("\n");
|
|
183
|
+
|
|
184
|
+
// Header should come before JSON
|
|
185
|
+
const headerIndex = lines.findIndex((line) => line.includes("// nax Configuration"));
|
|
186
|
+
const jsonIndex = lines.findIndex((line) => line.startsWith("{"));
|
|
187
|
+
|
|
188
|
+
expect(headerIndex).toBeGreaterThanOrEqual(0);
|
|
189
|
+
expect(jsonIndex).toBeGreaterThan(headerIndex);
|
|
190
|
+
|
|
191
|
+
// Should have blank line between header and JSON
|
|
192
|
+
expect(lines[jsonIndex - 1]).toBe("");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID Registry Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { PidRegistry } from "../../src/execution/pid-registry";
|
|
8
|
+
|
|
9
|
+
const TEST_WORKDIR = "/tmp/nax-pid-registry-test";
|
|
10
|
+
const PID_FILE = `${TEST_WORKDIR}/.nax-pids`;
|
|
11
|
+
|
|
12
|
+
describe("PidRegistry", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create test workdir
|
|
15
|
+
if (!existsSync(TEST_WORKDIR)) {
|
|
16
|
+
mkdirSync(TEST_WORKDIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Clean up any existing .nax-pids file
|
|
20
|
+
if (existsSync(PID_FILE)) {
|
|
21
|
+
rmSync(PID_FILE);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Cleanup test workdir
|
|
27
|
+
if (existsSync(TEST_WORKDIR)) {
|
|
28
|
+
rmSync(TEST_WORKDIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("register() adds PID to in-memory set and writes to file", async () => {
|
|
33
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
34
|
+
|
|
35
|
+
await registry.register(12345);
|
|
36
|
+
|
|
37
|
+
// Check in-memory state
|
|
38
|
+
expect(registry.getPids()).toEqual([12345]);
|
|
39
|
+
|
|
40
|
+
// Check file content
|
|
41
|
+
const content = await Bun.file(PID_FILE).text();
|
|
42
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
43
|
+
expect(lines.length).toBe(1);
|
|
44
|
+
|
|
45
|
+
const entry = JSON.parse(lines[0]);
|
|
46
|
+
expect(entry.pid).toBe(12345);
|
|
47
|
+
expect(entry.workdir).toBe(TEST_WORKDIR);
|
|
48
|
+
expect(entry.spawnedAt).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("register() appends multiple PIDs to file", async () => {
|
|
52
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
53
|
+
|
|
54
|
+
await registry.register(12345);
|
|
55
|
+
await registry.register(67890);
|
|
56
|
+
|
|
57
|
+
// Check in-memory state
|
|
58
|
+
expect(registry.getPids()).toEqual([12345, 67890]);
|
|
59
|
+
|
|
60
|
+
// Check file content
|
|
61
|
+
const content = await Bun.file(PID_FILE).text();
|
|
62
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
63
|
+
expect(lines.length).toBe(2);
|
|
64
|
+
|
|
65
|
+
const entry1 = JSON.parse(lines[0]);
|
|
66
|
+
expect(entry1.pid).toBe(12345);
|
|
67
|
+
|
|
68
|
+
const entry2 = JSON.parse(lines[1]);
|
|
69
|
+
expect(entry2.pid).toBe(67890);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("unregister() removes PID from in-memory set and rewrites file", async () => {
|
|
73
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
74
|
+
|
|
75
|
+
await registry.register(12345);
|
|
76
|
+
await registry.register(67890);
|
|
77
|
+
await registry.unregister(12345);
|
|
78
|
+
|
|
79
|
+
// Check in-memory state
|
|
80
|
+
expect(registry.getPids()).toEqual([67890]);
|
|
81
|
+
|
|
82
|
+
// Check file content
|
|
83
|
+
const content = await Bun.file(PID_FILE).text();
|
|
84
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
85
|
+
expect(lines.length).toBe(1);
|
|
86
|
+
|
|
87
|
+
const entry = JSON.parse(lines[0]);
|
|
88
|
+
expect(entry.pid).toBe(67890);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("unregister() clears file when last PID is removed", async () => {
|
|
92
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
93
|
+
|
|
94
|
+
await registry.register(12345);
|
|
95
|
+
await registry.unregister(12345);
|
|
96
|
+
|
|
97
|
+
// Check in-memory state
|
|
98
|
+
expect(registry.getPids()).toEqual([]);
|
|
99
|
+
|
|
100
|
+
// Check file is empty
|
|
101
|
+
const content = await Bun.file(PID_FILE).text();
|
|
102
|
+
expect(content.trim()).toBe("");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("killAll() clears in-memory PIDs and registry file", async () => {
|
|
106
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
107
|
+
|
|
108
|
+
// Register non-existent PIDs (will fail to kill but should clear registry)
|
|
109
|
+
await registry.register(99999);
|
|
110
|
+
await registry.register(88888);
|
|
111
|
+
|
|
112
|
+
await registry.killAll();
|
|
113
|
+
|
|
114
|
+
// Check in-memory state
|
|
115
|
+
expect(registry.getPids()).toEqual([]);
|
|
116
|
+
|
|
117
|
+
// Check file is empty
|
|
118
|
+
const content = await Bun.file(PID_FILE).text();
|
|
119
|
+
expect(content.trim()).toBe("");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("killAll() handles empty registry gracefully", async () => {
|
|
123
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
124
|
+
|
|
125
|
+
// Should not throw
|
|
126
|
+
await registry.killAll();
|
|
127
|
+
|
|
128
|
+
expect(registry.getPids()).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("cleanupStale() reads and kills PIDs from previous run", async () => {
|
|
132
|
+
// Simulate a previous run that left PIDs in the file
|
|
133
|
+
const entry1 = JSON.stringify({
|
|
134
|
+
pid: 99999, // Non-existent PID
|
|
135
|
+
spawnedAt: new Date().toISOString(),
|
|
136
|
+
workdir: TEST_WORKDIR,
|
|
137
|
+
});
|
|
138
|
+
const entry2 = JSON.stringify({
|
|
139
|
+
pid: 88888, // Non-existent PID
|
|
140
|
+
spawnedAt: new Date().toISOString(),
|
|
141
|
+
workdir: TEST_WORKDIR,
|
|
142
|
+
});
|
|
143
|
+
await Bun.write(PID_FILE, `${entry1}\n${entry2}\n`);
|
|
144
|
+
|
|
145
|
+
// Create new registry and cleanup stale PIDs
|
|
146
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
147
|
+
await registry.cleanupStale();
|
|
148
|
+
|
|
149
|
+
// Check file is cleared
|
|
150
|
+
const content = await Bun.file(PID_FILE).text();
|
|
151
|
+
expect(content.trim()).toBe("");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("cleanupStale() handles missing .nax-pids file", async () => {
|
|
155
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
156
|
+
|
|
157
|
+
// Should not throw
|
|
158
|
+
await registry.cleanupStale();
|
|
159
|
+
|
|
160
|
+
// File should not exist
|
|
161
|
+
expect(existsSync(PID_FILE)).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("cleanupStale() handles empty .nax-pids file", async () => {
|
|
165
|
+
// Create empty file
|
|
166
|
+
await Bun.write(PID_FILE, "");
|
|
167
|
+
|
|
168
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
169
|
+
await registry.cleanupStale();
|
|
170
|
+
|
|
171
|
+
// File should be empty
|
|
172
|
+
const content = await Bun.file(PID_FILE).text();
|
|
173
|
+
expect(content.trim()).toBe("");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("cleanupStale() handles malformed JSON lines gracefully", async () => {
|
|
177
|
+
// Write malformed JSON
|
|
178
|
+
await Bun.write(PID_FILE, 'not valid json\n{"pid":12345}\n');
|
|
179
|
+
|
|
180
|
+
const registry = new PidRegistry(TEST_WORKDIR);
|
|
181
|
+
|
|
182
|
+
// Should not throw
|
|
183
|
+
await registry.cleanupStale();
|
|
184
|
+
|
|
185
|
+
// File should be cleared
|
|
186
|
+
const content = await Bun.file(PID_FILE).text();
|
|
187
|
+
expect(content.trim()).toBe("");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("platform-specific kill command: Linux uses process groups", async () => {
|
|
191
|
+
const registry = new PidRegistry(TEST_WORKDIR, "linux");
|
|
192
|
+
|
|
193
|
+
// Register a non-existent PID
|
|
194
|
+
await registry.register(99999);
|
|
195
|
+
|
|
196
|
+
// Should not throw (process doesn't exist, but kill command should be correct)
|
|
197
|
+
await registry.killAll();
|
|
198
|
+
|
|
199
|
+
expect(registry.getPids()).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("platform-specific kill command: macOS uses direct PID", async () => {
|
|
203
|
+
const registry = new PidRegistry(TEST_WORKDIR, "darwin");
|
|
204
|
+
|
|
205
|
+
// Register a non-existent PID
|
|
206
|
+
await registry.register(99999);
|
|
207
|
+
|
|
208
|
+
// Should not throw (process doesn't exist, but kill command should be correct)
|
|
209
|
+
await registry.killAll();
|
|
210
|
+
|
|
211
|
+
expect(registry.getPids()).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("multiple registries can coexist with different workdirs", async () => {
|
|
215
|
+
const workdir1 = `${TEST_WORKDIR}/workspace1`;
|
|
216
|
+
const workdir2 = `${TEST_WORKDIR}/workspace2`;
|
|
217
|
+
|
|
218
|
+
mkdirSync(workdir1, { recursive: true });
|
|
219
|
+
mkdirSync(workdir2, { recursive: true });
|
|
220
|
+
|
|
221
|
+
const registry1 = new PidRegistry(workdir1);
|
|
222
|
+
const registry2 = new PidRegistry(workdir2);
|
|
223
|
+
|
|
224
|
+
await registry1.register(11111);
|
|
225
|
+
await registry2.register(22222);
|
|
226
|
+
|
|
227
|
+
expect(registry1.getPids()).toEqual([11111]);
|
|
228
|
+
expect(registry2.getPids()).toEqual([22222]);
|
|
229
|
+
|
|
230
|
+
// Check separate files
|
|
231
|
+
const content1 = await Bun.file(`${workdir1}/.nax-pids`).text();
|
|
232
|
+
const content2 = await Bun.file(`${workdir2}/.nax-pids`).text();
|
|
233
|
+
|
|
234
|
+
const entry1 = JSON.parse(content1.trim());
|
|
235
|
+
const entry2 = JSON.parse(content2.trim());
|
|
236
|
+
|
|
237
|
+
expect(entry1.pid).toBe(11111);
|
|
238
|
+
expect(entry2.pid).toBe(22222);
|
|
239
|
+
});
|
|
240
|
+
});
|