@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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the nax unlock command
|
|
3
|
+
*
|
|
4
|
+
* Covers all acceptance criteria:
|
|
5
|
+
* AC1: No lock file -> prints 'No lock file found', exits 0
|
|
6
|
+
* AC2: Lock PID alive -> prints error, exits 1, lock untouched
|
|
7
|
+
* AC3: Lock PID dead -> prints lock info (PID, age), removes lock, exits 0
|
|
8
|
+
* AC4: --force -> removes lock unconditionally, exits 0
|
|
9
|
+
* AC5: -d <path> -> targets specified directory (not cwd)
|
|
10
|
+
* AC6: Unit coverage of all four scenarios
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { unlockCommand } from "../../../src/commands/unlock";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Custom error to intercept process.exit without terminating the test runner
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class ExitError extends Error {
|
|
24
|
+
constructor(public readonly code: number) {
|
|
25
|
+
super(`process.exit(${code})`);
|
|
26
|
+
this.name = "ExitError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Test helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
async function writeLock(dir: string, pid: number, ageMs = 0): Promise<void> {
|
|
35
|
+
const lockData = { pid, timestamp: Date.now() - ageMs };
|
|
36
|
+
await Bun.write(join(dir, "nax.lock"), JSON.stringify(lockData));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Test suite
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
describe("unlockCommand", () => {
|
|
44
|
+
let testDir: string;
|
|
45
|
+
let capturedOutput: string[];
|
|
46
|
+
let capturedErrors: string[];
|
|
47
|
+
let exitCode: number | null;
|
|
48
|
+
|
|
49
|
+
const originalLog = console.log;
|
|
50
|
+
const originalError = console.error;
|
|
51
|
+
const originalExit = process.exit;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
const raw = join(tmpdir(), `nax-unlock-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
55
|
+
mkdirSync(raw, { recursive: true });
|
|
56
|
+
testDir = realpathSync(raw);
|
|
57
|
+
|
|
58
|
+
capturedOutput = [];
|
|
59
|
+
capturedErrors = [];
|
|
60
|
+
exitCode = null;
|
|
61
|
+
|
|
62
|
+
console.log = (...args: unknown[]) => {
|
|
63
|
+
capturedOutput.push(args.join(" "));
|
|
64
|
+
};
|
|
65
|
+
console.error = (...args: unknown[]) => {
|
|
66
|
+
capturedErrors.push(args.join(" "));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Intercept process.exit: record the code and throw so the command stops.
|
|
70
|
+
process.exit = ((code?: number) => {
|
|
71
|
+
exitCode = code ?? 0;
|
|
72
|
+
throw new ExitError(exitCode);
|
|
73
|
+
}) as never;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
console.log = originalLog;
|
|
78
|
+
console.error = originalError;
|
|
79
|
+
process.exit = originalExit;
|
|
80
|
+
|
|
81
|
+
if (existsSync(testDir)) {
|
|
82
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Helper: run unlockCommand, absorbing ExitError but re-throwing other errors.
|
|
87
|
+
async function run(options: Parameters<typeof unlockCommand>[0]): Promise<void> {
|
|
88
|
+
try {
|
|
89
|
+
await unlockCommand(options);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (!(err instanceof ExitError)) {
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function allOutput(): string {
|
|
98
|
+
return [...capturedOutput, ...capturedErrors].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// AC1: No lock file present
|
|
103
|
+
// =========================================================================
|
|
104
|
+
|
|
105
|
+
describe("AC1: no lock file", () => {
|
|
106
|
+
test("prints 'No lock file found' and exits 0", async () => {
|
|
107
|
+
await run({ dir: testDir });
|
|
108
|
+
|
|
109
|
+
expect(allOutput()).toContain("No lock file found");
|
|
110
|
+
// exit 0 means either natural return (exitCode null) or explicit exit(0)
|
|
111
|
+
expect(exitCode === null || exitCode === 0).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("does not create a lock file", async () => {
|
|
115
|
+
await run({ dir: testDir });
|
|
116
|
+
|
|
117
|
+
expect(existsSync(join(testDir, "nax.lock"))).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// =========================================================================
|
|
122
|
+
// AC2: Lock PID is alive — refuse to unlock
|
|
123
|
+
// =========================================================================
|
|
124
|
+
|
|
125
|
+
describe("AC2: lock PID is alive", () => {
|
|
126
|
+
test("prints 'nax is still running (PID <n>). Use --force to override.'", async () => {
|
|
127
|
+
// process.pid is the current test-runner process — always alive.
|
|
128
|
+
await writeLock(testDir, process.pid);
|
|
129
|
+
|
|
130
|
+
await run({ dir: testDir });
|
|
131
|
+
|
|
132
|
+
expect(allOutput()).toContain(`nax is still running (PID ${process.pid})`);
|
|
133
|
+
expect(allOutput()).toContain("--force");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("exits with code 1", async () => {
|
|
137
|
+
await writeLock(testDir, process.pid);
|
|
138
|
+
|
|
139
|
+
await run({ dir: testDir });
|
|
140
|
+
|
|
141
|
+
expect(exitCode).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("does NOT delete the lock file", async () => {
|
|
145
|
+
const lockPath = join(testDir, "nax.lock");
|
|
146
|
+
await writeLock(testDir, process.pid);
|
|
147
|
+
|
|
148
|
+
await run({ dir: testDir });
|
|
149
|
+
|
|
150
|
+
expect(existsSync(lockPath)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// =========================================================================
|
|
155
|
+
// AC3: Lock PID is dead — unlock and clean up
|
|
156
|
+
// =========================================================================
|
|
157
|
+
|
|
158
|
+
describe("AC3: lock PID is dead", () => {
|
|
159
|
+
// PID 999999 is astronomically unlikely to exist on any real system.
|
|
160
|
+
const DEAD_PID = 999999;
|
|
161
|
+
|
|
162
|
+
test("prints lock info including PID before removing", async () => {
|
|
163
|
+
await writeLock(testDir, DEAD_PID, 5 * 60 * 1000); // 5 minutes old
|
|
164
|
+
|
|
165
|
+
await run({ dir: testDir });
|
|
166
|
+
|
|
167
|
+
expect(allOutput()).toContain(String(DEAD_PID));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("prints lock age in minutes", async () => {
|
|
171
|
+
await writeLock(testDir, DEAD_PID, 5 * 60 * 1000); // 5 minutes old
|
|
172
|
+
|
|
173
|
+
await run({ dir: testDir });
|
|
174
|
+
|
|
175
|
+
// Output should mention age in minutes (e.g. "5 min" or "5 minutes")
|
|
176
|
+
expect(allOutput()).toMatch(/\d+\s*min/i);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("removes nax.lock", async () => {
|
|
180
|
+
const lockPath = join(testDir, "nax.lock");
|
|
181
|
+
await writeLock(testDir, DEAD_PID);
|
|
182
|
+
|
|
183
|
+
await run({ dir: testDir });
|
|
184
|
+
|
|
185
|
+
expect(existsSync(lockPath)).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("exits 0", async () => {
|
|
189
|
+
await writeLock(testDir, DEAD_PID);
|
|
190
|
+
|
|
191
|
+
await run({ dir: testDir });
|
|
192
|
+
|
|
193
|
+
expect(exitCode === null || exitCode === 0).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// =========================================================================
|
|
198
|
+
// AC4: --force flag — unconditional removal
|
|
199
|
+
// =========================================================================
|
|
200
|
+
|
|
201
|
+
describe("AC4: --force flag", () => {
|
|
202
|
+
test("removes lock even when PID is alive", async () => {
|
|
203
|
+
const lockPath = join(testDir, "nax.lock");
|
|
204
|
+
// process.pid is alive
|
|
205
|
+
await writeLock(testDir, process.pid);
|
|
206
|
+
|
|
207
|
+
await run({ dir: testDir, force: true });
|
|
208
|
+
|
|
209
|
+
expect(existsSync(lockPath)).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("exits 0 when lock was held by a live PID", async () => {
|
|
213
|
+
await writeLock(testDir, process.pid);
|
|
214
|
+
|
|
215
|
+
await run({ dir: testDir, force: true });
|
|
216
|
+
|
|
217
|
+
expect(exitCode === null || exitCode === 0).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("exits 0 when there is no lock file at all", async () => {
|
|
221
|
+
// No lock written — --force should still succeed gracefully
|
|
222
|
+
await run({ dir: testDir, force: true });
|
|
223
|
+
|
|
224
|
+
expect(exitCode === null || exitCode === 0).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("does not print the 'still running' refusal message", async () => {
|
|
228
|
+
await writeLock(testDir, process.pid);
|
|
229
|
+
|
|
230
|
+
await run({ dir: testDir, force: true });
|
|
231
|
+
|
|
232
|
+
expect(allOutput()).not.toContain("nax is still running");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// =========================================================================
|
|
237
|
+
// AC5: -d <path> flag — target a specific directory
|
|
238
|
+
// =========================================================================
|
|
239
|
+
|
|
240
|
+
describe("AC5: -d <path> targets the specified directory", () => {
|
|
241
|
+
test("reads lock from the specified directory, not cwd", async () => {
|
|
242
|
+
const altDir = realpathSync(
|
|
243
|
+
(() => {
|
|
244
|
+
const d = join(tmpdir(), `nax-unlock-alt-${Date.now()}`);
|
|
245
|
+
mkdirSync(d, { recursive: true });
|
|
246
|
+
return d;
|
|
247
|
+
})(),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const DEAD_PID = 999999;
|
|
251
|
+
await writeLock(altDir, DEAD_PID);
|
|
252
|
+
|
|
253
|
+
const altLockPath = join(altDir, "nax.lock");
|
|
254
|
+
const testDirLockPath = join(testDir, "nax.lock");
|
|
255
|
+
|
|
256
|
+
await run({ dir: altDir });
|
|
257
|
+
|
|
258
|
+
// The lock in altDir must be removed
|
|
259
|
+
expect(existsSync(altLockPath)).toBe(false);
|
|
260
|
+
// The (absent) lock in testDir must remain absent
|
|
261
|
+
expect(existsSync(testDirLockPath)).toBe(false);
|
|
262
|
+
|
|
263
|
+
rmSync(altDir, { recursive: true, force: true });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("ignores cwd when -d is provided and cwd has no lock", async () => {
|
|
267
|
+
// Put a lock ONLY in altDir; cwd (testDir) has no lock
|
|
268
|
+
const altDir = realpathSync(
|
|
269
|
+
(() => {
|
|
270
|
+
const d = join(tmpdir(), `nax-unlock-alt2-${Date.now()}`);
|
|
271
|
+
mkdirSync(d, { recursive: true });
|
|
272
|
+
return d;
|
|
273
|
+
})(),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// No lock in altDir either — just confirming it reads from altDir
|
|
277
|
+
await run({ dir: altDir });
|
|
278
|
+
|
|
279
|
+
// AC1 behaviour applies for the targeted dir (no lock file)
|
|
280
|
+
expect(allOutput()).toContain("No lock file found");
|
|
281
|
+
|
|
282
|
+
rmSync(altDir, { recursive: true, force: true });
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// =========================================================================
|
|
287
|
+
// AC6: Scenario matrix — all four core cases covered by unit tests
|
|
288
|
+
//
|
|
289
|
+
// (Verified by the tests above; this block provides explicit proof that
|
|
290
|
+
// each scenario class is addressed.)
|
|
291
|
+
// =========================================================================
|
|
292
|
+
|
|
293
|
+
describe("AC6: all four scenario classes covered", () => {
|
|
294
|
+
const DEAD_PID = 999999;
|
|
295
|
+
|
|
296
|
+
test("scenario: no lock", async () => {
|
|
297
|
+
await run({ dir: testDir });
|
|
298
|
+
expect(allOutput()).toContain("No lock file found");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("scenario: alive PID without --force", async () => {
|
|
302
|
+
await writeLock(testDir, process.pid);
|
|
303
|
+
await run({ dir: testDir });
|
|
304
|
+
expect(exitCode).toBe(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("scenario: dead PID without --force", async () => {
|
|
308
|
+
await writeLock(testDir, DEAD_PID);
|
|
309
|
+
await run({ dir: testDir });
|
|
310
|
+
expect(existsSync(join(testDir, "nax.lock"))).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("scenario: --force removes lock regardless of PID state", async () => {
|
|
314
|
+
await writeLock(testDir, process.pid);
|
|
315
|
+
await run({ dir: testDir, force: true });
|
|
316
|
+
expect(existsSync(join(testDir, "nax.lock"))).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constitution Generators Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for generating agent-specific config files from constitution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { aiderGenerator } from "../../src/constitution/generators/aider";
|
|
9
|
+
import { claudeGenerator } from "../../src/constitution/generators/claude";
|
|
10
|
+
import { cursorGenerator } from "../../src/constitution/generators/cursor";
|
|
11
|
+
import { opencodeGenerator } from "../../src/constitution/generators/opencode";
|
|
12
|
+
import type { ConstitutionContent } from "../../src/constitution/generators/types";
|
|
13
|
+
import { windsurfGenerator } from "../../src/constitution/generators/windsurf";
|
|
14
|
+
|
|
15
|
+
const sampleConstitution: ConstitutionContent = {
|
|
16
|
+
markdown: `# Project Constitution
|
|
17
|
+
|
|
18
|
+
## Coding Standards
|
|
19
|
+
- Follow TypeScript best practices
|
|
20
|
+
- Use strict typing
|
|
21
|
+
|
|
22
|
+
## Testing Requirements
|
|
23
|
+
- 80% minimum coverage
|
|
24
|
+
- Write tests first (TDD)
|
|
25
|
+
|
|
26
|
+
## Architecture Rules
|
|
27
|
+
- Single responsibility principle
|
|
28
|
+
- Dependency injection
|
|
29
|
+
`,
|
|
30
|
+
sections: {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("Constitution Generators", () => {
|
|
34
|
+
describe("Claude Generator", () => {
|
|
35
|
+
test("should generate CLAUDE.md with correct format", () => {
|
|
36
|
+
const result = claudeGenerator.generate(sampleConstitution);
|
|
37
|
+
|
|
38
|
+
expect(result).toContain("# Project Constitution");
|
|
39
|
+
expect(result).toContain("auto-generated from `nax/constitution.md`");
|
|
40
|
+
expect(result).toContain("DO NOT EDIT MANUALLY");
|
|
41
|
+
expect(result).toContain("## Coding Standards");
|
|
42
|
+
expect(result).toContain("Follow TypeScript best practices");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("should have correct output filename", () => {
|
|
46
|
+
expect(claudeGenerator.outputFile).toBe("CLAUDE.md");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should have correct generator name", () => {
|
|
50
|
+
expect(claudeGenerator.name).toBe("claude");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("OpenCode Generator", () => {
|
|
55
|
+
test("should generate AGENTS.md with correct format", () => {
|
|
56
|
+
const result = opencodeGenerator.generate(sampleConstitution);
|
|
57
|
+
|
|
58
|
+
expect(result).toContain("# Agent Instructions");
|
|
59
|
+
expect(result).toContain("auto-generated from `nax/constitution.md`");
|
|
60
|
+
expect(result).toContain("DO NOT EDIT MANUALLY");
|
|
61
|
+
expect(result).toContain("## Coding Standards");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("should have correct output filename", () => {
|
|
65
|
+
expect(opencodeGenerator.outputFile).toBe("AGENTS.md");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("should have correct generator name", () => {
|
|
69
|
+
expect(opencodeGenerator.name).toBe("opencode");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Cursor Generator", () => {
|
|
74
|
+
test("should generate .cursorrules with correct format", () => {
|
|
75
|
+
const result = cursorGenerator.generate(sampleConstitution);
|
|
76
|
+
|
|
77
|
+
expect(result).toContain("# Project Rules");
|
|
78
|
+
expect(result).toContain("Auto-generated from nax/constitution.md");
|
|
79
|
+
expect(result).toContain("DO NOT EDIT MANUALLY");
|
|
80
|
+
expect(result).toContain("## Coding Standards");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("should have correct output filename", () => {
|
|
84
|
+
expect(cursorGenerator.outputFile).toBe(".cursorrules");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("should have correct generator name", () => {
|
|
88
|
+
expect(cursorGenerator.name).toBe("cursor");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Windsurf Generator", () => {
|
|
93
|
+
test("should generate .windsurfrules with correct format", () => {
|
|
94
|
+
const result = windsurfGenerator.generate(sampleConstitution);
|
|
95
|
+
|
|
96
|
+
expect(result).toContain("# Windsurf Project Rules");
|
|
97
|
+
expect(result).toContain("Auto-generated from nax/constitution.md");
|
|
98
|
+
expect(result).toContain("DO NOT EDIT MANUALLY");
|
|
99
|
+
expect(result).toContain("## Coding Standards");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("should have correct output filename", () => {
|
|
103
|
+
expect(windsurfGenerator.outputFile).toBe(".windsurfrules");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("should have correct generator name", () => {
|
|
107
|
+
expect(windsurfGenerator.name).toBe("windsurf");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("Aider Generator", () => {
|
|
112
|
+
test("should generate .aider.conf.yml with correct YAML format", () => {
|
|
113
|
+
const result = aiderGenerator.generate(sampleConstitution);
|
|
114
|
+
|
|
115
|
+
expect(result).toContain("# Aider Configuration");
|
|
116
|
+
expect(result).toContain("# Auto-generated from nax/constitution.md");
|
|
117
|
+
expect(result).toContain("# DO NOT EDIT MANUALLY");
|
|
118
|
+
expect(result).toContain("instructions: |");
|
|
119
|
+
// Check YAML indentation
|
|
120
|
+
expect(result).toContain(" # Project Constitution");
|
|
121
|
+
expect(result).toContain(" ## Coding Standards");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("should have correct output filename", () => {
|
|
125
|
+
expect(aiderGenerator.outputFile).toBe(".aider.conf.yml");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("should have correct generator name", () => {
|
|
129
|
+
expect(aiderGenerator.name).toBe("aider");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("All Generators", () => {
|
|
134
|
+
test("should preserve original constitution content", () => {
|
|
135
|
+
const generators = [claudeGenerator, opencodeGenerator, cursorGenerator, windsurfGenerator, aiderGenerator];
|
|
136
|
+
|
|
137
|
+
for (const generator of generators) {
|
|
138
|
+
const result = generator.generate(sampleConstitution);
|
|
139
|
+
expect(result).toContain("Follow TypeScript best practices");
|
|
140
|
+
expect(result).toContain("80% minimum coverage");
|
|
141
|
+
expect(result).toContain("Single responsibility principle");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("should handle empty constitution", () => {
|
|
146
|
+
const emptyConstitution: ConstitutionContent = {
|
|
147
|
+
markdown: "",
|
|
148
|
+
sections: {},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const generators = [claudeGenerator, opencodeGenerator, cursorGenerator, windsurfGenerator, aiderGenerator];
|
|
152
|
+
|
|
153
|
+
for (const generator of generators) {
|
|
154
|
+
const result = generator.generate(emptyConstitution);
|
|
155
|
+
// Should still have header
|
|
156
|
+
expect(result.length).toBeGreaterThan(0);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constitution system tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { estimateTokens, loadConstitution, truncateToTokens } from "../../src/constitution";
|
|
9
|
+
import type { ConstitutionConfig } from "../../src/constitution";
|
|
10
|
+
|
|
11
|
+
const TEST_DIR = join(import.meta.dir, ".tmp-constitution-test");
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
if (existsSync(TEST_DIR)) {
|
|
15
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (existsSync(TEST_DIR)) {
|
|
22
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("estimateTokens", () => {
|
|
27
|
+
test("estimates tokens using 1 token ≈ 3 chars", () => {
|
|
28
|
+
expect(estimateTokens("abc")).toBe(1); // 3 chars = 1 token
|
|
29
|
+
expect(estimateTokens("abcdef")).toBe(2); // 6 chars = 2 tokens
|
|
30
|
+
expect(estimateTokens("a".repeat(100))).toBe(34); // 100 chars = 34 tokens (rounded up)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("handles empty string", () => {
|
|
34
|
+
expect(estimateTokens("")).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("rounds up fractional tokens", () => {
|
|
38
|
+
expect(estimateTokens("ab")).toBe(1); // 2 chars = 0.67 tokens → rounds up to 1
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("truncateToTokens", () => {
|
|
43
|
+
test("returns full text if within token limit", () => {
|
|
44
|
+
const text = "Hello world";
|
|
45
|
+
const result = truncateToTokens(text, 100);
|
|
46
|
+
expect(result).toBe(text);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("truncates at word boundary", () => {
|
|
50
|
+
const text = "The quick brown fox jumps over the lazy dog";
|
|
51
|
+
const result = truncateToTokens(text, 5); // 5 tokens ≈ 15 chars
|
|
52
|
+
expect(result.length).toBeLessThanOrEqual(15);
|
|
53
|
+
expect(result).not.toContain("fox"); // Should stop before "fox"
|
|
54
|
+
// Result should be "The quick" which ends with a word character
|
|
55
|
+
expect(result.trim()).toBe("The quick");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("truncates at newline boundary", () => {
|
|
59
|
+
const text = "Line 1\nLine 2\nLine 3\nLine 4";
|
|
60
|
+
const result = truncateToTokens(text, 3); // 3 tokens ≈ 9 chars
|
|
61
|
+
expect(result).toContain("Line 1");
|
|
62
|
+
expect(result).not.toContain("Line 3");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("hard cuts if no word boundary found", () => {
|
|
66
|
+
const text = "a".repeat(100);
|
|
67
|
+
const result = truncateToTokens(text, 5); // 5 tokens ≈ 15 chars
|
|
68
|
+
expect(result.length).toBe(15);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("loadConstitution", () => {
|
|
73
|
+
test("returns null if disabled", async () => {
|
|
74
|
+
const config: ConstitutionConfig = {
|
|
75
|
+
enabled: false,
|
|
76
|
+
path: "constitution.md",
|
|
77
|
+
maxTokens: 2000,
|
|
78
|
+
skipGlobal: true,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
82
|
+
expect(result).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("returns null if file doesn't exist", async () => {
|
|
86
|
+
const config: ConstitutionConfig = {
|
|
87
|
+
enabled: true,
|
|
88
|
+
path: "constitution.md",
|
|
89
|
+
maxTokens: 2000,
|
|
90
|
+
skipGlobal: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
94
|
+
expect(result).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns null if file is empty", async () => {
|
|
98
|
+
const constitutionPath = join(TEST_DIR, "constitution.md");
|
|
99
|
+
await Bun.write(constitutionPath, " \n\n "); // Only whitespace
|
|
100
|
+
|
|
101
|
+
const config: ConstitutionConfig = {
|
|
102
|
+
enabled: true,
|
|
103
|
+
path: "constitution.md",
|
|
104
|
+
maxTokens: 2000,
|
|
105
|
+
skipGlobal: true,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
109
|
+
expect(result).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("loads constitution without truncation", async () => {
|
|
113
|
+
const content = "# Project Constitution\n\nFollow these rules.";
|
|
114
|
+
const constitutionPath = join(TEST_DIR, "constitution.md");
|
|
115
|
+
await Bun.write(constitutionPath, content);
|
|
116
|
+
|
|
117
|
+
const config: ConstitutionConfig = {
|
|
118
|
+
enabled: true,
|
|
119
|
+
path: "constitution.md",
|
|
120
|
+
maxTokens: 2000,
|
|
121
|
+
skipGlobal: true,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
125
|
+
expect(result).not.toBeNull();
|
|
126
|
+
expect(result?.content).toBe(content);
|
|
127
|
+
expect(result?.tokens).toBe(estimateTokens(content));
|
|
128
|
+
expect(result?.truncated).toBe(false);
|
|
129
|
+
expect(result?.originalTokens).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("truncates constitution if exceeds maxTokens", async () => {
|
|
133
|
+
const content = "A".repeat(300); // 300 chars = 100 tokens
|
|
134
|
+
const constitutionPath = join(TEST_DIR, "constitution.md");
|
|
135
|
+
await Bun.write(constitutionPath, content);
|
|
136
|
+
|
|
137
|
+
const config: ConstitutionConfig = {
|
|
138
|
+
enabled: true,
|
|
139
|
+
path: "constitution.md",
|
|
140
|
+
maxTokens: 50, // Only allow 50 tokens
|
|
141
|
+
skipGlobal: true,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
145
|
+
expect(result).not.toBeNull();
|
|
146
|
+
expect(result?.truncated).toBe(true);
|
|
147
|
+
expect(result?.tokens).toBeLessThanOrEqual(50);
|
|
148
|
+
expect(result?.originalTokens).toBe(100);
|
|
149
|
+
expect(result?.content.length).toBeLessThan(content.length);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("loads from custom path", async () => {
|
|
153
|
+
const content = "# Custom Constitution";
|
|
154
|
+
const customPath = join(TEST_DIR, "custom-rules.md");
|
|
155
|
+
await Bun.write(customPath, content);
|
|
156
|
+
|
|
157
|
+
const config: ConstitutionConfig = {
|
|
158
|
+
enabled: true,
|
|
159
|
+
path: "custom-rules.md",
|
|
160
|
+
maxTokens: 2000,
|
|
161
|
+
skipGlobal: true,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
165
|
+
expect(result).not.toBeNull();
|
|
166
|
+
expect(result?.content).toBe(content);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("handles large constitution with meaningful content", async () => {
|
|
170
|
+
const content = `# Project Constitution
|
|
171
|
+
|
|
172
|
+
## Coding Standards
|
|
173
|
+
- Use TypeScript strict mode
|
|
174
|
+
- Follow ESLint rules
|
|
175
|
+
- Write clear variable names
|
|
176
|
+
|
|
177
|
+
## Testing
|
|
178
|
+
- Write unit tests for all functions
|
|
179
|
+
- Aim for 80%+ coverage
|
|
180
|
+
- Use describe/test/expect pattern
|
|
181
|
+
|
|
182
|
+
## Architecture
|
|
183
|
+
- Keep functions small (<50 lines)
|
|
184
|
+
- Use dependency injection
|
|
185
|
+
- Follow SOLID principles
|
|
186
|
+
|
|
187
|
+
## Forbidden Patterns
|
|
188
|
+
- No any types
|
|
189
|
+
- No console.log
|
|
190
|
+
- No hardcoded secrets
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const constitutionPath = join(TEST_DIR, "constitution.md");
|
|
194
|
+
await Bun.write(constitutionPath, content);
|
|
195
|
+
|
|
196
|
+
const config: ConstitutionConfig = {
|
|
197
|
+
enabled: true,
|
|
198
|
+
path: "constitution.md",
|
|
199
|
+
maxTokens: 2000,
|
|
200
|
+
skipGlobal: true,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = await loadConstitution(TEST_DIR, config);
|
|
204
|
+
expect(result).not.toBeNull();
|
|
205
|
+
expect(result?.content).toBe(content);
|
|
206
|
+
expect(result?.truncated).toBe(false);
|
|
207
|
+
expect(result?.tokens).toBeGreaterThan(0);
|
|
208
|
+
});
|
|
209
|
+
});
|