@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,937 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing Tests
|
|
3
|
+
*
|
|
4
|
+
* Consolidated test suite for routing system including:
|
|
5
|
+
* - Core routing logic (classifyComplexity, determineTestStrategy, routeTask)
|
|
6
|
+
* - Routing strategies (keyword, llm, manual, adaptive)
|
|
7
|
+
* - Strategy chain execution
|
|
8
|
+
* - Async support and chain delegation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
12
|
+
import { DEFAULT_CONFIG } from "../../src/config";
|
|
13
|
+
import type { NaxConfig } from "../../src/config";
|
|
14
|
+
import { escalateTier } from "../../src/execution/runner";
|
|
15
|
+
import type { AggregateMetrics } from "../../src/metrics/types";
|
|
16
|
+
import type { UserStory } from "../../src/prd/types";
|
|
17
|
+
import { classifyComplexity, determineTestStrategy, routeTask } from "../../src/routing";
|
|
18
|
+
import { buildStrategyChain } from "../../src/routing/builder";
|
|
19
|
+
import { StrategyChain } from "../../src/routing/chain";
|
|
20
|
+
import { keywordStrategy, llmStrategy, manualStrategy } from "../../src/routing/strategies";
|
|
21
|
+
import { adaptiveStrategy } from "../../src/routing/strategies/adaptive";
|
|
22
|
+
import {
|
|
23
|
+
buildBatchPrompt,
|
|
24
|
+
buildRoutingPrompt,
|
|
25
|
+
clearCache,
|
|
26
|
+
llmStrategy as llmStrategyFull,
|
|
27
|
+
parseRoutingResponse,
|
|
28
|
+
routeBatch,
|
|
29
|
+
stripCodeFences,
|
|
30
|
+
validateRoutingDecision,
|
|
31
|
+
} from "../../src/routing/strategies/llm";
|
|
32
|
+
import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../../src/routing/strategy";
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Core Routing Logic Tests
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
describe("classifyComplexity", () => {
|
|
39
|
+
test("simple: few criteria, no keywords", () => {
|
|
40
|
+
expect(classifyComplexity("Fix typo", "Fix a typo in error message", ["Typo is fixed"], [])).toBe("simple");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("medium: moderate criteria count", () => {
|
|
44
|
+
expect(classifyComplexity("Add validation", "Add DTO validation", ["a", "b", "c", "d", "e"], [])).toBe("medium");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("complex: security keyword", () => {
|
|
48
|
+
expect(classifyComplexity("Auth refactor", "Refactor JWT authentication", ["Token works"], ["security"])).toBe(
|
|
49
|
+
"complex",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("expert: distributed keyword", () => {
|
|
54
|
+
expect(classifyComplexity("Real-time sync", "Real-time distributed consensus", ["Sync works"], [])).toBe("expert");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("4 ACs should classify as simple (BUG-19 regression)", () => {
|
|
58
|
+
const complexity = classifyComplexity(
|
|
59
|
+
"Add validation",
|
|
60
|
+
"Add basic input validation",
|
|
61
|
+
["AC1", "AC2", "AC3", "AC4"],
|
|
62
|
+
[],
|
|
63
|
+
);
|
|
64
|
+
expect(complexity).toBe("simple");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("5 ACs should classify as medium (BUG-19 regression)", () => {
|
|
68
|
+
const complexity = classifyComplexity(
|
|
69
|
+
"Add validation",
|
|
70
|
+
"Add comprehensive input validation",
|
|
71
|
+
["AC1", "AC2", "AC3", "AC4", "AC5"],
|
|
72
|
+
[],
|
|
73
|
+
);
|
|
74
|
+
expect(complexity).toBe("medium");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("9 ACs should classify as complex (BUG-19 regression)", () => {
|
|
78
|
+
const complexity = classifyComplexity(
|
|
79
|
+
"Add validation",
|
|
80
|
+
"Add extensive input validation",
|
|
81
|
+
["AC1", "AC2", "AC3", "AC4", "AC5", "AC6", "AC7", "AC8", "AC9"],
|
|
82
|
+
[],
|
|
83
|
+
);
|
|
84
|
+
expect(complexity).toBe("complex");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("determineTestStrategy", () => {
|
|
89
|
+
test("simple → test-after", () => {
|
|
90
|
+
expect(determineTestStrategy("simple", "Fix typo", "Fix a typo", [])).toBe("test-after");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("complex → three-session-tdd", () => {
|
|
94
|
+
expect(determineTestStrategy("complex", "Refactor module", "Complex refactor", [])).toBe("three-session-tdd");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("security keyword → three-session-tdd even if simple", () => {
|
|
98
|
+
expect(determineTestStrategy("simple", "Fix auth bypass", "Security fix for JWT token", ["security"])).toBe(
|
|
99
|
+
"three-session-tdd",
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("public api keyword → three-session-tdd even if simple", () => {
|
|
104
|
+
expect(determineTestStrategy("simple", "Add endpoint", "New public api endpoint for users", [])).toBe(
|
|
105
|
+
"three-session-tdd",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("tddStrategy overrides", () => {
|
|
110
|
+
test("strategy='strict' always returns three-session-tdd", () => {
|
|
111
|
+
expect(determineTestStrategy("simple", "Update button", "Change color", [], "strict")).toBe("three-session-tdd");
|
|
112
|
+
expect(determineTestStrategy("medium", "Update button", "Change color", [], "strict")).toBe("three-session-tdd");
|
|
113
|
+
expect(determineTestStrategy("complex", "Refactor module", "Big refactor", [], "strict")).toBe(
|
|
114
|
+
"three-session-tdd",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("strategy='lite' always returns three-session-tdd-lite", () => {
|
|
119
|
+
expect(determineTestStrategy("simple", "Update button", "Change color", [], "lite")).toBe(
|
|
120
|
+
"three-session-tdd-lite",
|
|
121
|
+
);
|
|
122
|
+
expect(determineTestStrategy("medium", "Update form", "Add validation", [], "lite")).toBe(
|
|
123
|
+
"three-session-tdd-lite",
|
|
124
|
+
);
|
|
125
|
+
expect(determineTestStrategy("complex", "Refactor module", "Big refactor", [], "lite")).toBe(
|
|
126
|
+
"three-session-tdd-lite",
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("strategy='off' always returns test-after", () => {
|
|
131
|
+
expect(determineTestStrategy("simple", "Update button", "Change color", [], "off")).toBe("test-after");
|
|
132
|
+
expect(determineTestStrategy("complex", "Refactor auth", "JWT refactor", ["security"], "off")).toBe("test-after");
|
|
133
|
+
expect(determineTestStrategy("expert", "Real-time sync", "Distributed consensus", [], "off")).toBe("test-after");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("strategy='auto' returns three-session-tdd-lite for UI-tagged complex stories", () => {
|
|
137
|
+
expect(determineTestStrategy("complex", "Redesign dashboard", "UI overhaul", ["ui"], "auto")).toBe(
|
|
138
|
+
"three-session-tdd-lite",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("strategy='auto' returns three-session-tdd-lite for layout-tagged stories", () => {
|
|
143
|
+
expect(determineTestStrategy("complex", "Fix layout", "Responsive layout fix", ["layout"], "auto")).toBe(
|
|
144
|
+
"three-session-tdd-lite",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("strategy='auto' security-critical stories always return three-session-tdd even with ui tag", () => {
|
|
149
|
+
expect(determineTestStrategy("complex", "Auth UI", "JWT token security screen", ["ui", "security"], "auto")).toBe(
|
|
150
|
+
"three-session-tdd",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("strategy='auto' lite tags are case-insensitive", () => {
|
|
155
|
+
expect(determineTestStrategy("complex", "Build UI", "Create UI", ["UI"], "auto")).toBe("three-session-tdd-lite");
|
|
156
|
+
expect(determineTestStrategy("complex", "Build CLI", "Create CLI", ["CLI"], "auto")).toBe(
|
|
157
|
+
"three-session-tdd-lite",
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("routeTask", () => {
|
|
164
|
+
test("routes simple task to fast model with test-after", () => {
|
|
165
|
+
const result = routeTask("Fix typo", "Fix a typo", ["Typo fixed"], [], DEFAULT_CONFIG);
|
|
166
|
+
expect(result.complexity).toBe("simple");
|
|
167
|
+
expect(result.modelTier).toBe("fast");
|
|
168
|
+
expect(result.testStrategy).toBe("test-after");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("routes security task to powerful with three-session-tdd", () => {
|
|
172
|
+
const result = routeTask("Auth fix", "Fix JWT auth bypass", ["Auth works"], ["security"], DEFAULT_CONFIG);
|
|
173
|
+
expect(result.complexity).toBe("complex");
|
|
174
|
+
expect(result.modelTier).toBe("powerful");
|
|
175
|
+
expect(result.testStrategy).toBe("three-session-tdd");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("routes all complexity levels correctly", () => {
|
|
179
|
+
const simpleResult = routeTask("Fix typo", "Fix a typo", ["Typo fixed"], [], DEFAULT_CONFIG);
|
|
180
|
+
expect(simpleResult.complexity).toBe("simple");
|
|
181
|
+
expect(simpleResult.modelTier).toBe("fast");
|
|
182
|
+
|
|
183
|
+
const mediumResult = routeTask(
|
|
184
|
+
"Add validation",
|
|
185
|
+
"Add DTO validation",
|
|
186
|
+
["a", "b", "c", "d", "e"],
|
|
187
|
+
[],
|
|
188
|
+
DEFAULT_CONFIG,
|
|
189
|
+
);
|
|
190
|
+
expect(mediumResult.complexity).toBe("medium");
|
|
191
|
+
expect(mediumResult.modelTier).toBe("balanced");
|
|
192
|
+
|
|
193
|
+
const complexResult = routeTask(
|
|
194
|
+
"Auth refactor",
|
|
195
|
+
"Refactor JWT authentication",
|
|
196
|
+
["Token works"],
|
|
197
|
+
["security"],
|
|
198
|
+
DEFAULT_CONFIG,
|
|
199
|
+
);
|
|
200
|
+
expect(complexResult.complexity).toBe("complex");
|
|
201
|
+
expect(complexResult.modelTier).toBe("powerful");
|
|
202
|
+
|
|
203
|
+
const expertResult = routeTask(
|
|
204
|
+
"Real-time sync",
|
|
205
|
+
"Real-time distributed consensus",
|
|
206
|
+
["Sync works"],
|
|
207
|
+
[],
|
|
208
|
+
DEFAULT_CONFIG,
|
|
209
|
+
);
|
|
210
|
+
expect(expertResult.complexity).toBe("expert");
|
|
211
|
+
expect(expertResult.modelTier).toBe("powerful");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("complexity → modelTier mapping respects config (BUG-19 regression)", () => {
|
|
215
|
+
const simpleResult = routeTask("Simple task", "Simple description", ["AC1"], [], DEFAULT_CONFIG);
|
|
216
|
+
expect(simpleResult.complexity).toBe("simple");
|
|
217
|
+
expect(simpleResult.modelTier).toBe("fast");
|
|
218
|
+
|
|
219
|
+
const mediumResult = routeTask(
|
|
220
|
+
"Medium task",
|
|
221
|
+
"Medium description",
|
|
222
|
+
["AC1", "AC2", "AC3", "AC4", "AC5"],
|
|
223
|
+
[],
|
|
224
|
+
DEFAULT_CONFIG,
|
|
225
|
+
);
|
|
226
|
+
expect(mediumResult.complexity).toBe("medium");
|
|
227
|
+
expect(mediumResult.modelTier).toBe("balanced");
|
|
228
|
+
|
|
229
|
+
const complexResult = routeTask(
|
|
230
|
+
"Complex task",
|
|
231
|
+
"Complex description",
|
|
232
|
+
["AC1", "AC2", "AC3", "AC4", "AC5", "AC6", "AC7", "AC8", "AC9"],
|
|
233
|
+
[],
|
|
234
|
+
DEFAULT_CONFIG,
|
|
235
|
+
);
|
|
236
|
+
expect(complexResult.complexity).toBe("complex");
|
|
237
|
+
expect(complexResult.modelTier).toBe("powerful");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("tddStrategy config integration", () => {
|
|
241
|
+
const makeConfig = (strategy: NaxConfig["tdd"]["strategy"]): NaxConfig => ({
|
|
242
|
+
...DEFAULT_CONFIG,
|
|
243
|
+
tdd: { ...DEFAULT_CONFIG.tdd, strategy },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("config.tdd.strategy='strict' forces three-session-tdd on simple task", () => {
|
|
247
|
+
const result = routeTask("Fix typo", "Fix a typo", ["Typo fixed"], [], makeConfig("strict"));
|
|
248
|
+
expect(result.testStrategy).toBe("three-session-tdd");
|
|
249
|
+
expect(result.reasoning).toContain("strategy:strict");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("config.tdd.strategy='lite' forces three-session-tdd-lite on any task", () => {
|
|
253
|
+
const result = routeTask("Fix typo", "Fix a typo", ["Typo fixed"], [], makeConfig("lite"));
|
|
254
|
+
expect(result.testStrategy).toBe("three-session-tdd-lite");
|
|
255
|
+
expect(result.reasoning).toContain("strategy:lite");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("config.tdd.strategy='off' forces test-after even on complex/security tasks", () => {
|
|
259
|
+
const result = routeTask("Auth refactor", "JWT auth security", ["Token works"], ["security"], makeConfig("off"));
|
|
260
|
+
expect(result.testStrategy).toBe("test-after");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("default config (strategy='auto') preserves existing routing behavior", () => {
|
|
264
|
+
const simpleResult = routeTask("Fix typo", "Fix a typo", ["Typo fixed"], [], DEFAULT_CONFIG);
|
|
265
|
+
expect(simpleResult.testStrategy).toBe("test-after");
|
|
266
|
+
|
|
267
|
+
const complexResult = routeTask(
|
|
268
|
+
"Auth refactor",
|
|
269
|
+
"Refactor JWT authentication",
|
|
270
|
+
["Token works"],
|
|
271
|
+
["security"],
|
|
272
|
+
DEFAULT_CONFIG,
|
|
273
|
+
);
|
|
274
|
+
expect(complexResult.testStrategy).toBe("three-session-tdd");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("escalateTier", () => {
|
|
280
|
+
const defaultTiers = [
|
|
281
|
+
{ tier: "fast", attempts: 5 },
|
|
282
|
+
{ tier: "balanced", attempts: 3 },
|
|
283
|
+
{ tier: "powerful", attempts: 2 },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
test("escalates fast → balanced", () => {
|
|
287
|
+
expect(escalateTier("fast", defaultTiers)).toBe("balanced");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("escalates balanced → powerful", () => {
|
|
291
|
+
expect(escalateTier("balanced", defaultTiers)).toBe("powerful");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("escalates powerful → null (max reached)", () => {
|
|
295
|
+
expect(escalateTier("powerful", defaultTiers)).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("explicit 3-tier escalation chain: fast → balanced → powerful → null", () => {
|
|
299
|
+
let tier: string | null = escalateTier("fast", defaultTiers);
|
|
300
|
+
expect(tier).toBe("balanced");
|
|
301
|
+
|
|
302
|
+
tier = escalateTier(tier!, defaultTiers);
|
|
303
|
+
expect(tier).toBe("powerful");
|
|
304
|
+
|
|
305
|
+
tier = escalateTier(tier!, defaultTiers);
|
|
306
|
+
expect(tier).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Strategy System Tests
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
describe("StrategyChain", () => {
|
|
315
|
+
test("uses first strategy that returns non-null", async () => {
|
|
316
|
+
const alwaysNullStrategy: RoutingStrategy = {
|
|
317
|
+
name: "always-null",
|
|
318
|
+
route: () => null,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const alwaysReturnStrategy: RoutingStrategy = {
|
|
322
|
+
name: "always-return",
|
|
323
|
+
route: () => ({
|
|
324
|
+
complexity: "simple",
|
|
325
|
+
modelTier: "fast",
|
|
326
|
+
testStrategy: "test-after",
|
|
327
|
+
reasoning: "Always return strategy",
|
|
328
|
+
}),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const chain = new StrategyChain([alwaysNullStrategy, alwaysReturnStrategy]);
|
|
332
|
+
|
|
333
|
+
const story: UserStory = {
|
|
334
|
+
id: "US-001",
|
|
335
|
+
title: "Test story",
|
|
336
|
+
description: "Test",
|
|
337
|
+
acceptanceCriteria: [],
|
|
338
|
+
tags: [],
|
|
339
|
+
dependencies: [],
|
|
340
|
+
status: "pending",
|
|
341
|
+
passes: false,
|
|
342
|
+
escalations: [],
|
|
343
|
+
attempts: 0,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
347
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
348
|
+
const decision = await chain.route(story, context);
|
|
349
|
+
|
|
350
|
+
expect(decision.reasoning).toBe("Always return strategy");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("throws error if all strategies return null", async () => {
|
|
354
|
+
const alwaysNullStrategy: RoutingStrategy = {
|
|
355
|
+
name: "always-null",
|
|
356
|
+
route: () => null,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const chain = new StrategyChain([alwaysNullStrategy]);
|
|
360
|
+
|
|
361
|
+
const story: UserStory = {
|
|
362
|
+
id: "US-001",
|
|
363
|
+
title: "Test story",
|
|
364
|
+
description: "Test",
|
|
365
|
+
acceptanceCriteria: [],
|
|
366
|
+
tags: [],
|
|
367
|
+
dependencies: [],
|
|
368
|
+
status: "pending",
|
|
369
|
+
passes: false,
|
|
370
|
+
escalations: [],
|
|
371
|
+
attempts: 0,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
375
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
376
|
+
|
|
377
|
+
await expect(chain.route(story, context)).rejects.toThrow("No routing strategy returned a decision");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("getStrategyNames returns strategy names", () => {
|
|
381
|
+
const chain = new StrategyChain([keywordStrategy, llmStrategy]);
|
|
382
|
+
expect(chain.getStrategyNames()).toEqual(["keyword", "llm"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe("async support", () => {
|
|
386
|
+
test("handles async strategy that returns decision", async () => {
|
|
387
|
+
const asyncStrategy: RoutingStrategy = {
|
|
388
|
+
name: "async-test",
|
|
389
|
+
route: async () => {
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
391
|
+
return {
|
|
392
|
+
complexity: "medium",
|
|
393
|
+
modelTier: "balanced",
|
|
394
|
+
testStrategy: "test-after",
|
|
395
|
+
reasoning: "Async strategy result",
|
|
396
|
+
};
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const chain = new StrategyChain([asyncStrategy]);
|
|
401
|
+
|
|
402
|
+
const story: UserStory = {
|
|
403
|
+
id: "US-001",
|
|
404
|
+
title: "Test async story",
|
|
405
|
+
description: "Test async routing",
|
|
406
|
+
acceptanceCriteria: [],
|
|
407
|
+
tags: [],
|
|
408
|
+
dependencies: [],
|
|
409
|
+
status: "pending",
|
|
410
|
+
passes: false,
|
|
411
|
+
escalations: [],
|
|
412
|
+
attempts: 0,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const context: RoutingContext = { config: DEFAULT_CONFIG };
|
|
416
|
+
const decision = await chain.route(story, context);
|
|
417
|
+
|
|
418
|
+
expect(decision.reasoning).toBe("Async strategy result");
|
|
419
|
+
expect(decision.complexity).toBe("medium");
|
|
420
|
+
expect(decision.modelTier).toBe("balanced");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("handles mixed sync and async strategies", async () => {
|
|
424
|
+
const syncStrategy: RoutingStrategy = {
|
|
425
|
+
name: "sync-first",
|
|
426
|
+
route: () => null,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const asyncStrategy: RoutingStrategy = {
|
|
430
|
+
name: "async-second",
|
|
431
|
+
route: async () => {
|
|
432
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
433
|
+
return {
|
|
434
|
+
complexity: "complex",
|
|
435
|
+
modelTier: "powerful",
|
|
436
|
+
testStrategy: "three-session-tdd",
|
|
437
|
+
reasoning: "Mixed chain result",
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const chain = new StrategyChain([syncStrategy, asyncStrategy]);
|
|
443
|
+
|
|
444
|
+
const story: UserStory = {
|
|
445
|
+
id: "US-003",
|
|
446
|
+
title: "Test mixed",
|
|
447
|
+
description: "Test mixed sync/async",
|
|
448
|
+
acceptanceCriteria: [],
|
|
449
|
+
tags: [],
|
|
450
|
+
dependencies: [],
|
|
451
|
+
status: "pending",
|
|
452
|
+
passes: false,
|
|
453
|
+
escalations: [],
|
|
454
|
+
attempts: 0,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const context: RoutingContext = { config: DEFAULT_CONFIG };
|
|
458
|
+
const decision = await chain.route(story, context);
|
|
459
|
+
|
|
460
|
+
expect(decision.reasoning).toBe("Mixed chain result");
|
|
461
|
+
expect(decision.testStrategy).toBe("three-session-tdd");
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("keywordStrategy", () => {
|
|
467
|
+
test("classifies simple story correctly", () => {
|
|
468
|
+
const story: UserStory = {
|
|
469
|
+
id: "US-001",
|
|
470
|
+
title: "Update button color",
|
|
471
|
+
description: "Change button to blue",
|
|
472
|
+
acceptanceCriteria: ["Button is blue"],
|
|
473
|
+
tags: [],
|
|
474
|
+
dependencies: [],
|
|
475
|
+
status: "pending",
|
|
476
|
+
passes: false,
|
|
477
|
+
escalations: [],
|
|
478
|
+
attempts: 0,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
482
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
483
|
+
const decision = keywordStrategy.route(story, context);
|
|
484
|
+
|
|
485
|
+
expect(decision).not.toBeNull();
|
|
486
|
+
expect(decision!.complexity).toBe("simple");
|
|
487
|
+
expect(decision!.modelTier).toBe("fast");
|
|
488
|
+
expect(decision!.testStrategy).toBe("test-after");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("classifies complex story with security keywords", () => {
|
|
492
|
+
const story: UserStory = {
|
|
493
|
+
id: "US-002",
|
|
494
|
+
title: "Add JWT authentication",
|
|
495
|
+
description: "Implement JWT auth with refresh tokens",
|
|
496
|
+
acceptanceCriteria: ["Token storage", "Refresh logic", "Expiry"],
|
|
497
|
+
tags: ["security", "auth"],
|
|
498
|
+
dependencies: [],
|
|
499
|
+
status: "pending",
|
|
500
|
+
passes: false,
|
|
501
|
+
escalations: [],
|
|
502
|
+
attempts: 0,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
506
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
507
|
+
const decision = keywordStrategy.route(story, context);
|
|
508
|
+
|
|
509
|
+
expect(decision).not.toBeNull();
|
|
510
|
+
expect(decision!.complexity).toBe("complex");
|
|
511
|
+
expect(decision!.modelTier).toBe("powerful");
|
|
512
|
+
expect(decision!.testStrategy).toBe("three-session-tdd");
|
|
513
|
+
expect(decision!.reasoning).toContain("security-critical");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("uses three-session-tdd for public API", () => {
|
|
517
|
+
const story: UserStory = {
|
|
518
|
+
id: "US-005",
|
|
519
|
+
title: "Add public API endpoint",
|
|
520
|
+
description: "Create external API for consumers",
|
|
521
|
+
acceptanceCriteria: ["Endpoint returns JSON"],
|
|
522
|
+
tags: ["public api"],
|
|
523
|
+
dependencies: [],
|
|
524
|
+
status: "pending",
|
|
525
|
+
passes: false,
|
|
526
|
+
escalations: [],
|
|
527
|
+
attempts: 0,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
531
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
532
|
+
const decision = keywordStrategy.route(story, context);
|
|
533
|
+
|
|
534
|
+
expect(decision).not.toBeNull();
|
|
535
|
+
expect(decision!.testStrategy).toBe("three-session-tdd");
|
|
536
|
+
expect(decision!.reasoning).toContain("public-api");
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe("manualStrategy", () => {
|
|
541
|
+
test("returns decision from story.routing metadata", () => {
|
|
542
|
+
const story: UserStory = {
|
|
543
|
+
id: "US-006",
|
|
544
|
+
title: "Manual override test",
|
|
545
|
+
description: "Story with manual routing",
|
|
546
|
+
acceptanceCriteria: [],
|
|
547
|
+
tags: [],
|
|
548
|
+
dependencies: [],
|
|
549
|
+
status: "pending",
|
|
550
|
+
passes: false,
|
|
551
|
+
escalations: [],
|
|
552
|
+
attempts: 0,
|
|
553
|
+
routing: {
|
|
554
|
+
complexity: "expert",
|
|
555
|
+
modelTier: "powerful",
|
|
556
|
+
testStrategy: "three-session-tdd",
|
|
557
|
+
reasoning: "Manual override for critical task",
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
562
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
563
|
+
const decision = manualStrategy.route(story, context);
|
|
564
|
+
|
|
565
|
+
expect(decision).not.toBeNull();
|
|
566
|
+
expect(decision!.complexity).toBe("expert");
|
|
567
|
+
expect(decision!.modelTier).toBe("powerful");
|
|
568
|
+
expect(decision!.testStrategy).toBe("three-session-tdd");
|
|
569
|
+
expect(decision!.reasoning).toBe("Manual override for critical task");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("returns null when no routing metadata", () => {
|
|
573
|
+
const story: UserStory = {
|
|
574
|
+
id: "US-007",
|
|
575
|
+
title: "No manual routing",
|
|
576
|
+
description: "Story without routing metadata",
|
|
577
|
+
acceptanceCriteria: [],
|
|
578
|
+
tags: [],
|
|
579
|
+
dependencies: [],
|
|
580
|
+
status: "pending",
|
|
581
|
+
passes: false,
|
|
582
|
+
escalations: [],
|
|
583
|
+
attempts: 0,
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const configWithoutLlm = { ...DEFAULT_CONFIG, routing: { ...DEFAULT_CONFIG.routing, llm: undefined } };
|
|
587
|
+
const context: RoutingContext = { config: configWithoutLlm };
|
|
588
|
+
const decision = manualStrategy.route(story, context);
|
|
589
|
+
|
|
590
|
+
expect(decision).toBeNull();
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe("buildStrategyChain", () => {
|
|
595
|
+
test("builds keyword-only chain by default", async () => {
|
|
596
|
+
const chain = await buildStrategyChain(DEFAULT_CONFIG, "/tmp");
|
|
597
|
+
expect(chain.getStrategyNames()).toEqual(["keyword"]);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("builds manual + keyword chain when strategy=manual", async () => {
|
|
601
|
+
const config = {
|
|
602
|
+
...DEFAULT_CONFIG,
|
|
603
|
+
routing: { strategy: "manual" as const },
|
|
604
|
+
};
|
|
605
|
+
const chain = await buildStrategyChain(config, "/tmp");
|
|
606
|
+
expect(chain.getStrategyNames()).toEqual(["manual", "keyword"]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("builds llm + keyword chain when strategy=llm", async () => {
|
|
610
|
+
const config = {
|
|
611
|
+
...DEFAULT_CONFIG,
|
|
612
|
+
routing: { strategy: "llm" as const },
|
|
613
|
+
};
|
|
614
|
+
const chain = await buildStrategyChain(config, "/tmp");
|
|
615
|
+
expect(chain.getStrategyNames()).toEqual(["llm", "keyword"]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("throws error when custom strategy without path", async () => {
|
|
619
|
+
const config = {
|
|
620
|
+
...DEFAULT_CONFIG,
|
|
621
|
+
routing: { strategy: "custom" as const },
|
|
622
|
+
};
|
|
623
|
+
await expect(buildStrategyChain(config, "/tmp")).rejects.toThrow("routing.customStrategyPath is required");
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ============================================================================
|
|
628
|
+
// LLM Strategy Tests
|
|
629
|
+
// ============================================================================
|
|
630
|
+
|
|
631
|
+
// Test user stories for LLM tests
|
|
632
|
+
const simpleStory: UserStory = {
|
|
633
|
+
id: "US-001",
|
|
634
|
+
title: "Fix typo in README",
|
|
635
|
+
description: "Correct spelling mistake",
|
|
636
|
+
acceptanceCriteria: ["Update README.md with correct spelling"],
|
|
637
|
+
tags: ["docs"],
|
|
638
|
+
dependencies: [],
|
|
639
|
+
status: "pending",
|
|
640
|
+
passes: false,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const complexStory: UserStory = {
|
|
644
|
+
id: "US-002",
|
|
645
|
+
title: "Add JWT authentication",
|
|
646
|
+
description: "Implement JWT authentication with refresh tokens",
|
|
647
|
+
acceptanceCriteria: ["Secure token storage", "Token refresh endpoint", "Expiry handling", "Logout functionality"],
|
|
648
|
+
tags: ["security", "auth"],
|
|
649
|
+
dependencies: [],
|
|
650
|
+
status: "pending",
|
|
651
|
+
passes: false,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const testContext: RoutingContext = {
|
|
655
|
+
config: DEFAULT_CONFIG,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
describe("LLM Routing Strategy - Prompt Building", () => {
|
|
659
|
+
test("buildRoutingPrompt formats story correctly", () => {
|
|
660
|
+
const prompt = buildRoutingPrompt(simpleStory, DEFAULT_CONFIG);
|
|
661
|
+
|
|
662
|
+
expect(prompt).toContain("Title: Fix typo in README");
|
|
663
|
+
expect(prompt).toContain("Description: Correct spelling mistake");
|
|
664
|
+
expect(prompt).toContain("1. Update README.md with correct spelling");
|
|
665
|
+
expect(prompt).toContain("Tags: docs");
|
|
666
|
+
expect(prompt).toContain("fast: Simple changes");
|
|
667
|
+
expect(prompt).toContain("balanced: Standard features");
|
|
668
|
+
expect(prompt).toContain("powerful: Complex architecture");
|
|
669
|
+
expect(prompt).toContain("test-after: Write implementation first");
|
|
670
|
+
expect(prompt).toContain("three-session-tdd: Separate test-writer");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("buildBatchPrompt formats multiple stories", () => {
|
|
674
|
+
const stories = [simpleStory, complexStory];
|
|
675
|
+
const prompt = buildBatchPrompt(stories, DEFAULT_CONFIG);
|
|
676
|
+
|
|
677
|
+
expect(prompt).toContain("1. US-001: Fix typo in README");
|
|
678
|
+
expect(prompt).toContain("2. US-002: Add JWT authentication");
|
|
679
|
+
expect(prompt).toContain("Tags: docs");
|
|
680
|
+
expect(prompt).toContain("Tags: security, auth");
|
|
681
|
+
expect(prompt).toContain('{"id":"US-001"');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
describe("LLM Routing Strategy - Response Parsing", () => {
|
|
686
|
+
test("parseRoutingResponse handles valid JSON", () => {
|
|
687
|
+
const output =
|
|
688
|
+
'{"complexity":"simple","modelTier":"fast","testStrategy":"test-after","reasoning":"Simple documentation fix"}';
|
|
689
|
+
const decision = parseRoutingResponse(output, simpleStory, DEFAULT_CONFIG);
|
|
690
|
+
|
|
691
|
+
expect(decision.complexity).toBe("simple");
|
|
692
|
+
expect(decision.modelTier).toBe("fast");
|
|
693
|
+
expect(decision.testStrategy).toBe("test-after");
|
|
694
|
+
expect(decision.reasoning).toBe("Simple documentation fix");
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("parseRoutingResponse strips markdown code blocks", () => {
|
|
698
|
+
const output =
|
|
699
|
+
'```json\n{"complexity":"complex","modelTier":"powerful","testStrategy":"three-session-tdd","reasoning":"Security-critical"}\n```';
|
|
700
|
+
const decision = parseRoutingResponse(output, complexStory, DEFAULT_CONFIG);
|
|
701
|
+
|
|
702
|
+
expect(decision.complexity).toBe("complex");
|
|
703
|
+
expect(decision.modelTier).toBe("powerful");
|
|
704
|
+
expect(decision.testStrategy).toBe("three-session-tdd");
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("parseRoutingResponse throws on invalid JSON", () => {
|
|
708
|
+
const output = "This is not JSON";
|
|
709
|
+
expect(() => parseRoutingResponse(output, simpleStory, DEFAULT_CONFIG)).toThrow();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("parseRoutingResponse throws on missing fields", () => {
|
|
713
|
+
const output = '{"complexity":"simple","modelTier":"fast"}';
|
|
714
|
+
expect(() => parseRoutingResponse(output, simpleStory, DEFAULT_CONFIG)).toThrow("Missing required fields");
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("stripCodeFences", () => {
|
|
719
|
+
test("returns plain JSON unchanged", () => {
|
|
720
|
+
const input = '{"complexity":"simple"}';
|
|
721
|
+
expect(stripCodeFences(input)).toBe('{"complexity":"simple"}');
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("strips ```json ... ``` fences", () => {
|
|
725
|
+
const input = '```json\n{"complexity":"simple"}\n```';
|
|
726
|
+
expect(stripCodeFences(input)).toBe('{"complexity":"simple"}');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test("strips leading 'json' keyword (no backticks)", () => {
|
|
730
|
+
const input = 'json\n{"complexity":"simple"}';
|
|
731
|
+
expect(stripCodeFences(input)).toBe('{"complexity":"simple"}');
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
describe("validateRoutingDecision", () => {
|
|
736
|
+
test("returns valid decision for correct input", () => {
|
|
737
|
+
const input = { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "trivial" };
|
|
738
|
+
const result = validateRoutingDecision(input, DEFAULT_CONFIG);
|
|
739
|
+
expect(result).toEqual({
|
|
740
|
+
complexity: "simple",
|
|
741
|
+
modelTier: "fast",
|
|
742
|
+
testStrategy: "test-after",
|
|
743
|
+
reasoning: "trivial",
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("throws on missing complexity", () => {
|
|
748
|
+
const input = { modelTier: "fast", testStrategy: "test-after", reasoning: "test" };
|
|
749
|
+
expect(() => validateRoutingDecision(input, DEFAULT_CONFIG)).toThrow("Missing required fields");
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("throws on invalid complexity value", () => {
|
|
753
|
+
const input = { complexity: "mega", modelTier: "fast", testStrategy: "test-after", reasoning: "test" };
|
|
754
|
+
expect(() => validateRoutingDecision(input, DEFAULT_CONFIG)).toThrow("Invalid complexity: mega");
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// ============================================================================
|
|
759
|
+
// Adaptive Strategy Tests (Pure Logic)
|
|
760
|
+
// ============================================================================
|
|
761
|
+
|
|
762
|
+
function createStory(
|
|
763
|
+
id: string,
|
|
764
|
+
title: string,
|
|
765
|
+
description: string,
|
|
766
|
+
acceptanceCriteria: string[] = [],
|
|
767
|
+
tags: string[] = [],
|
|
768
|
+
): UserStory {
|
|
769
|
+
return {
|
|
770
|
+
id,
|
|
771
|
+
title,
|
|
772
|
+
description,
|
|
773
|
+
acceptanceCriteria,
|
|
774
|
+
tags,
|
|
775
|
+
status: "pending",
|
|
776
|
+
dependencies: [],
|
|
777
|
+
passes: false,
|
|
778
|
+
escalations: [],
|
|
779
|
+
attempts: 0,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function createContext(metrics?: AggregateMetrics, config: NaxConfig = DEFAULT_CONFIG): RoutingContext {
|
|
784
|
+
return {
|
|
785
|
+
config,
|
|
786
|
+
metrics,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function createMockMetrics(
|
|
791
|
+
complexityData: Record<string, { predicted: number; actualTierUsed: string; mismatchRate: number }>,
|
|
792
|
+
): AggregateMetrics {
|
|
793
|
+
return {
|
|
794
|
+
totalRuns: 10,
|
|
795
|
+
totalCost: 5.0,
|
|
796
|
+
totalStories: 100,
|
|
797
|
+
firstPassRate: 0.75,
|
|
798
|
+
escalationRate: 0.25,
|
|
799
|
+
avgCostPerStory: 0.05,
|
|
800
|
+
avgCostPerFeature: 0.5,
|
|
801
|
+
modelEfficiency: {
|
|
802
|
+
"claude-haiku-4-5": {
|
|
803
|
+
attempts: 60,
|
|
804
|
+
successes: 50,
|
|
805
|
+
passRate: 0.833,
|
|
806
|
+
avgCost: 0.005,
|
|
807
|
+
totalCost: 0.25,
|
|
808
|
+
},
|
|
809
|
+
"claude-sonnet-4.5": {
|
|
810
|
+
attempts: 30,
|
|
811
|
+
successes: 28,
|
|
812
|
+
passRate: 0.933,
|
|
813
|
+
avgCost: 0.02,
|
|
814
|
+
totalCost: 0.56,
|
|
815
|
+
},
|
|
816
|
+
"claude-opus-4-6": {
|
|
817
|
+
attempts: 10,
|
|
818
|
+
successes: 10,
|
|
819
|
+
passRate: 1.0,
|
|
820
|
+
avgCost: 0.08,
|
|
821
|
+
totalCost: 0.8,
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
complexityAccuracy: complexityData,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
describe("Adaptive Routing Strategy", () => {
|
|
829
|
+
describe("No metrics available", () => {
|
|
830
|
+
test("should fallback to configured strategy when no metrics", async () => {
|
|
831
|
+
const story = createStory("US-001", "Add user login", "Implement user authentication", [
|
|
832
|
+
"User can log in with email and password",
|
|
833
|
+
]);
|
|
834
|
+
|
|
835
|
+
const context = createContext(undefined);
|
|
836
|
+
const decision = await adaptiveStrategy.route(story, context);
|
|
837
|
+
|
|
838
|
+
expect(decision).not.toBeNull();
|
|
839
|
+
expect(decision?.reasoning).toContain("no metrics available");
|
|
840
|
+
expect(decision?.reasoning).toContain("fallback to");
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
describe("Insufficient data fallback", () => {
|
|
845
|
+
test("should fallback when samples below minSamples threshold", async () => {
|
|
846
|
+
const metrics = createMockMetrics({
|
|
847
|
+
simple: {
|
|
848
|
+
predicted: 5,
|
|
849
|
+
actualTierUsed: "fast",
|
|
850
|
+
mismatchRate: 0.2,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
const story = createStory("US-002", "Fix typo", "Fix typo in README", ["Typo is fixed"]);
|
|
855
|
+
|
|
856
|
+
const context = createContext(metrics);
|
|
857
|
+
const decision = await adaptiveStrategy.route(story, context);
|
|
858
|
+
|
|
859
|
+
expect(decision).not.toBeNull();
|
|
860
|
+
expect(decision?.reasoning).toContain("insufficient data");
|
|
861
|
+
expect(decision?.reasoning).toContain("5/10 samples");
|
|
862
|
+
expect(decision?.reasoning).toContain("fallback to");
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe("Sufficient data - adaptive routing", () => {
|
|
867
|
+
test("should route to fast tier when low mismatch rate", async () => {
|
|
868
|
+
const metrics = createMockMetrics({
|
|
869
|
+
simple: {
|
|
870
|
+
predicted: 50,
|
|
871
|
+
actualTierUsed: "fast",
|
|
872
|
+
mismatchRate: 0.1,
|
|
873
|
+
},
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const story = createStory("US-004", "Add button", "Add a submit button to the form", [
|
|
877
|
+
"Button is visible",
|
|
878
|
+
"Button triggers submit",
|
|
879
|
+
]);
|
|
880
|
+
|
|
881
|
+
const context = createContext(metrics);
|
|
882
|
+
const decision = await adaptiveStrategy.route(story, context);
|
|
883
|
+
|
|
884
|
+
expect(decision).not.toBeNull();
|
|
885
|
+
expect(decision?.complexity).toBe("simple");
|
|
886
|
+
expect(decision?.modelTier).toBe("fast");
|
|
887
|
+
expect(decision?.reasoning).toContain("adaptive");
|
|
888
|
+
expect(decision?.reasoning).toContain("simple → fast");
|
|
889
|
+
expect(decision?.reasoning).toContain("samples: 50");
|
|
890
|
+
expect(decision?.reasoning).toContain("mismatch: 10.0%");
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
test("should include cost information in reasoning", async () => {
|
|
894
|
+
const metrics = createMockMetrics({
|
|
895
|
+
complex: {
|
|
896
|
+
predicted: 15,
|
|
897
|
+
actualTierUsed: "powerful",
|
|
898
|
+
mismatchRate: 0.2,
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const story = createStory(
|
|
903
|
+
"US-006",
|
|
904
|
+
"Refactor authentication",
|
|
905
|
+
"Refactor the auth module to use JWT",
|
|
906
|
+
Array.from({ length: 10 }, (_, i) => `Criterion ${i + 1}`),
|
|
907
|
+
["security", "breaking-change"],
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
const context = createContext(metrics);
|
|
911
|
+
const decision = await adaptiveStrategy.route(story, context);
|
|
912
|
+
|
|
913
|
+
expect(decision).not.toBeNull();
|
|
914
|
+
expect(decision?.reasoning).toContain("cost:");
|
|
915
|
+
expect(decision?.reasoning).toMatch(/\$\d+\.\d{4}/);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
describe("Edge cases", () => {
|
|
920
|
+
test("should handle zero mismatch rate gracefully", async () => {
|
|
921
|
+
const metrics = createMockMetrics({
|
|
922
|
+
simple: {
|
|
923
|
+
predicted: 100,
|
|
924
|
+
actualTierUsed: "fast",
|
|
925
|
+
mismatchRate: 0.0,
|
|
926
|
+
},
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const story = createStory("US-014", "Add text", "Add help text", ["Text added"]);
|
|
930
|
+
const context = createContext(metrics);
|
|
931
|
+
const decision = await adaptiveStrategy.route(story, context);
|
|
932
|
+
|
|
933
|
+
expect(decision).not.toBeNull();
|
|
934
|
+
expect(decision?.modelTier).toBe("fast");
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
});
|