@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,1679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner Tests — Story Batching + TDD Escalation
|
|
3
|
+
*
|
|
4
|
+
* Tests for grouping consecutive simple stories into batches,
|
|
5
|
+
* and TDD escalation handling (retryAsLite, failure category outcomes).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { groupStoriesIntoBatches, precomputeBatchPlan } from "../../src/execution/batching";
|
|
10
|
+
import type { StoryBatch } from "../../src/execution/batching";
|
|
11
|
+
import { escalateTier } from "../../src/execution/escalation";
|
|
12
|
+
import { buildBatchPrompt } from "../../src/execution/prompts";
|
|
13
|
+
import { resolveMaxAttemptsOutcome } from "../../src/execution/runner";
|
|
14
|
+
import type { UserStory } from "../../src/prd";
|
|
15
|
+
import type { FailureCategory } from "../../src/tdd/types";
|
|
16
|
+
|
|
17
|
+
describe("buildBatchPrompt", () => {
|
|
18
|
+
test("generates prompt with multiple stories", () => {
|
|
19
|
+
const stories: UserStory[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "US-001",
|
|
22
|
+
title: "Add logging",
|
|
23
|
+
description: "Add debug logging to the service",
|
|
24
|
+
acceptanceCriteria: ["Logs are written", "Logs include timestamps"],
|
|
25
|
+
tags: [],
|
|
26
|
+
dependencies: [],
|
|
27
|
+
status: "pending",
|
|
28
|
+
passes: false,
|
|
29
|
+
escalations: [],
|
|
30
|
+
attempts: 0,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "US-002",
|
|
34
|
+
title: "Update config",
|
|
35
|
+
description: "Update the config schema",
|
|
36
|
+
acceptanceCriteria: ["Schema is valid", "Tests pass"],
|
|
37
|
+
tags: [],
|
|
38
|
+
dependencies: [],
|
|
39
|
+
status: "pending",
|
|
40
|
+
passes: false,
|
|
41
|
+
escalations: [],
|
|
42
|
+
attempts: 0,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const prompt = buildBatchPrompt(stories);
|
|
47
|
+
|
|
48
|
+
expect(prompt).toContain("Batch Task: 2 Stories");
|
|
49
|
+
expect(prompt).toContain("## Story 1: US-001 — Add logging");
|
|
50
|
+
expect(prompt).toContain("## Story 2: US-002 — Update config");
|
|
51
|
+
expect(prompt).toContain("Add debug logging to the service");
|
|
52
|
+
expect(prompt).toContain("Update the config schema");
|
|
53
|
+
expect(prompt).toContain("Commit each story separately");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("includes context markdown when provided", () => {
|
|
57
|
+
const stories: UserStory[] = [
|
|
58
|
+
{
|
|
59
|
+
id: "US-001",
|
|
60
|
+
title: "Add logging",
|
|
61
|
+
description: "Add logging",
|
|
62
|
+
acceptanceCriteria: ["Logs work"],
|
|
63
|
+
tags: [],
|
|
64
|
+
dependencies: [],
|
|
65
|
+
status: "pending",
|
|
66
|
+
passes: false,
|
|
67
|
+
escalations: [],
|
|
68
|
+
attempts: 0,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const contextMarkdown = "## Context\n\nSome context here";
|
|
73
|
+
const prompt = buildBatchPrompt(stories, contextMarkdown);
|
|
74
|
+
|
|
75
|
+
expect(prompt).toContain("---");
|
|
76
|
+
expect(prompt).toContain("## Context");
|
|
77
|
+
expect(prompt).toContain("Some context here");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("numbers stories sequentially", () => {
|
|
81
|
+
const stories: UserStory[] = [
|
|
82
|
+
{
|
|
83
|
+
id: "US-001",
|
|
84
|
+
title: "First",
|
|
85
|
+
description: "First story",
|
|
86
|
+
acceptanceCriteria: ["AC1"],
|
|
87
|
+
tags: [],
|
|
88
|
+
dependencies: [],
|
|
89
|
+
status: "pending",
|
|
90
|
+
passes: false,
|
|
91
|
+
escalations: [],
|
|
92
|
+
attempts: 0,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "US-002",
|
|
96
|
+
title: "Second",
|
|
97
|
+
description: "Second story",
|
|
98
|
+
acceptanceCriteria: ["AC2"],
|
|
99
|
+
tags: [],
|
|
100
|
+
dependencies: [],
|
|
101
|
+
status: "pending",
|
|
102
|
+
passes: false,
|
|
103
|
+
escalations: [],
|
|
104
|
+
attempts: 0,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "US-003",
|
|
108
|
+
title: "Third",
|
|
109
|
+
description: "Third story",
|
|
110
|
+
acceptanceCriteria: ["AC3"],
|
|
111
|
+
tags: [],
|
|
112
|
+
dependencies: [],
|
|
113
|
+
status: "pending",
|
|
114
|
+
passes: false,
|
|
115
|
+
escalations: [],
|
|
116
|
+
attempts: 0,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const prompt = buildBatchPrompt(stories);
|
|
121
|
+
|
|
122
|
+
expect(prompt).toContain("Story 1: US-001");
|
|
123
|
+
expect(prompt).toContain("Story 2: US-002");
|
|
124
|
+
expect(prompt).toContain("Story 3: US-003");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("groupStoriesIntoBatches", () => {
|
|
129
|
+
test("groups consecutive simple stories into a batch", () => {
|
|
130
|
+
const stories: UserStory[] = [
|
|
131
|
+
{
|
|
132
|
+
id: "US-001",
|
|
133
|
+
title: "Simple 1",
|
|
134
|
+
description: "First simple story",
|
|
135
|
+
acceptanceCriteria: ["AC1"],
|
|
136
|
+
tags: [],
|
|
137
|
+
dependencies: [],
|
|
138
|
+
status: "pending",
|
|
139
|
+
passes: false,
|
|
140
|
+
escalations: [],
|
|
141
|
+
attempts: 0,
|
|
142
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "US-002",
|
|
146
|
+
title: "Simple 2",
|
|
147
|
+
description: "Second simple story",
|
|
148
|
+
acceptanceCriteria: ["AC2"],
|
|
149
|
+
tags: [],
|
|
150
|
+
dependencies: [],
|
|
151
|
+
status: "pending",
|
|
152
|
+
passes: false,
|
|
153
|
+
escalations: [],
|
|
154
|
+
attempts: 0,
|
|
155
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "US-003",
|
|
159
|
+
title: "Simple 3",
|
|
160
|
+
description: "Third simple story",
|
|
161
|
+
acceptanceCriteria: ["AC3"],
|
|
162
|
+
tags: [],
|
|
163
|
+
dependencies: [],
|
|
164
|
+
status: "pending",
|
|
165
|
+
passes: false,
|
|
166
|
+
escalations: [],
|
|
167
|
+
attempts: 0,
|
|
168
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
173
|
+
|
|
174
|
+
expect(batches).toHaveLength(1);
|
|
175
|
+
expect(batches[0].isBatch).toBe(true);
|
|
176
|
+
expect(batches[0].stories).toHaveLength(3);
|
|
177
|
+
expect(batches[0].stories.map((s) => s.id)).toEqual(["US-001", "US-002", "US-003"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("enforces max batch size of 4", () => {
|
|
181
|
+
const stories: UserStory[] = Array.from({ length: 6 }, (_, i) => ({
|
|
182
|
+
id: `US-00${i + 1}`,
|
|
183
|
+
title: `Simple ${i + 1}`,
|
|
184
|
+
description: `Story ${i + 1}`,
|
|
185
|
+
acceptanceCriteria: [`AC${i + 1}`],
|
|
186
|
+
tags: [],
|
|
187
|
+
dependencies: [],
|
|
188
|
+
status: "pending" as const,
|
|
189
|
+
passes: false,
|
|
190
|
+
escalations: [],
|
|
191
|
+
attempts: 0,
|
|
192
|
+
routing: {
|
|
193
|
+
complexity: "simple" as const,
|
|
194
|
+
modelTier: "fast" as const,
|
|
195
|
+
testStrategy: "test-after" as const,
|
|
196
|
+
reasoning: "simple",
|
|
197
|
+
},
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
201
|
+
|
|
202
|
+
expect(batches).toHaveLength(2);
|
|
203
|
+
expect(batches[0].stories).toHaveLength(4);
|
|
204
|
+
expect(batches[0].isBatch).toBe(true);
|
|
205
|
+
expect(batches[1].stories).toHaveLength(2);
|
|
206
|
+
expect(batches[1].isBatch).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("stops batching at non-simple story", () => {
|
|
210
|
+
const stories: UserStory[] = [
|
|
211
|
+
{
|
|
212
|
+
id: "US-001",
|
|
213
|
+
title: "Simple 1",
|
|
214
|
+
description: "First simple",
|
|
215
|
+
acceptanceCriteria: ["AC1"],
|
|
216
|
+
tags: [],
|
|
217
|
+
dependencies: [],
|
|
218
|
+
status: "pending",
|
|
219
|
+
passes: false,
|
|
220
|
+
escalations: [],
|
|
221
|
+
attempts: 0,
|
|
222
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "US-002",
|
|
226
|
+
title: "Simple 2",
|
|
227
|
+
description: "Second simple",
|
|
228
|
+
acceptanceCriteria: ["AC2"],
|
|
229
|
+
tags: [],
|
|
230
|
+
dependencies: [],
|
|
231
|
+
status: "pending",
|
|
232
|
+
passes: false,
|
|
233
|
+
escalations: [],
|
|
234
|
+
attempts: 0,
|
|
235
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "US-003",
|
|
239
|
+
title: "Complex",
|
|
240
|
+
description: "Complex story",
|
|
241
|
+
acceptanceCriteria: ["AC3"],
|
|
242
|
+
tags: [],
|
|
243
|
+
dependencies: [],
|
|
244
|
+
status: "pending",
|
|
245
|
+
passes: false,
|
|
246
|
+
escalations: [],
|
|
247
|
+
attempts: 0,
|
|
248
|
+
routing: {
|
|
249
|
+
complexity: "complex",
|
|
250
|
+
modelTier: "balanced",
|
|
251
|
+
testStrategy: "three-session-tdd",
|
|
252
|
+
reasoning: "complex",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "US-004",
|
|
257
|
+
title: "Simple 3",
|
|
258
|
+
description: "Third simple",
|
|
259
|
+
acceptanceCriteria: ["AC4"],
|
|
260
|
+
tags: [],
|
|
261
|
+
dependencies: [],
|
|
262
|
+
status: "pending",
|
|
263
|
+
passes: false,
|
|
264
|
+
escalations: [],
|
|
265
|
+
attempts: 0,
|
|
266
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
271
|
+
|
|
272
|
+
expect(batches).toHaveLength(3);
|
|
273
|
+
// First batch: 2 simple stories
|
|
274
|
+
expect(batches[0].stories).toHaveLength(2);
|
|
275
|
+
expect(batches[0].isBatch).toBe(true);
|
|
276
|
+
expect(batches[0].stories.map((s) => s.id)).toEqual(["US-001", "US-002"]);
|
|
277
|
+
// Second batch: 1 complex story (not batched)
|
|
278
|
+
expect(batches[1].stories).toHaveLength(1);
|
|
279
|
+
expect(batches[1].isBatch).toBe(false);
|
|
280
|
+
expect(batches[1].stories[0].id).toBe("US-003");
|
|
281
|
+
// Third batch: 1 simple story (single, marked as batch)
|
|
282
|
+
expect(batches[2].stories).toHaveLength(1);
|
|
283
|
+
expect(batches[2].isBatch).toBe(false);
|
|
284
|
+
expect(batches[2].stories[0].id).toBe("US-004");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("handles single story as non-batch", () => {
|
|
288
|
+
const stories: UserStory[] = [
|
|
289
|
+
{
|
|
290
|
+
id: "US-001",
|
|
291
|
+
title: "Simple",
|
|
292
|
+
description: "Single story",
|
|
293
|
+
acceptanceCriteria: ["AC1"],
|
|
294
|
+
tags: [],
|
|
295
|
+
dependencies: [],
|
|
296
|
+
status: "pending",
|
|
297
|
+
passes: false,
|
|
298
|
+
escalations: [],
|
|
299
|
+
attempts: 0,
|
|
300
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
305
|
+
|
|
306
|
+
expect(batches).toHaveLength(1);
|
|
307
|
+
expect(batches[0].isBatch).toBe(false);
|
|
308
|
+
expect(batches[0].stories).toHaveLength(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("handles all non-simple stories", () => {
|
|
312
|
+
const stories: UserStory[] = [
|
|
313
|
+
{
|
|
314
|
+
id: "US-001",
|
|
315
|
+
title: "Complex 1",
|
|
316
|
+
description: "First complex",
|
|
317
|
+
acceptanceCriteria: ["AC1"],
|
|
318
|
+
tags: [],
|
|
319
|
+
dependencies: [],
|
|
320
|
+
status: "pending",
|
|
321
|
+
passes: false,
|
|
322
|
+
escalations: [],
|
|
323
|
+
attempts: 0,
|
|
324
|
+
routing: {
|
|
325
|
+
complexity: "complex",
|
|
326
|
+
modelTier: "balanced",
|
|
327
|
+
testStrategy: "three-session-tdd",
|
|
328
|
+
reasoning: "complex",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "US-002",
|
|
333
|
+
title: "Medium",
|
|
334
|
+
description: "Medium story",
|
|
335
|
+
acceptanceCriteria: ["AC2"],
|
|
336
|
+
tags: [],
|
|
337
|
+
dependencies: [],
|
|
338
|
+
status: "pending",
|
|
339
|
+
passes: false,
|
|
340
|
+
escalations: [],
|
|
341
|
+
attempts: 0,
|
|
342
|
+
routing: { complexity: "medium", modelTier: "fast", testStrategy: "test-after", reasoning: "medium" },
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
347
|
+
|
|
348
|
+
expect(batches).toHaveLength(2);
|
|
349
|
+
expect(batches[0].isBatch).toBe(false);
|
|
350
|
+
expect(batches[0].stories).toHaveLength(1);
|
|
351
|
+
expect(batches[1].isBatch).toBe(false);
|
|
352
|
+
expect(batches[1].stories).toHaveLength(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("handles empty story list", () => {
|
|
356
|
+
const stories: UserStory[] = [];
|
|
357
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
358
|
+
|
|
359
|
+
expect(batches).toHaveLength(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("respects custom max batch size", () => {
|
|
363
|
+
const stories: UserStory[] = Array.from({ length: 5 }, (_, i) => ({
|
|
364
|
+
id: `US-00${i + 1}`,
|
|
365
|
+
title: `Simple ${i + 1}`,
|
|
366
|
+
description: `Story ${i + 1}`,
|
|
367
|
+
acceptanceCriteria: [`AC${i + 1}`],
|
|
368
|
+
tags: [],
|
|
369
|
+
dependencies: [],
|
|
370
|
+
status: "pending" as const,
|
|
371
|
+
passes: false,
|
|
372
|
+
escalations: [],
|
|
373
|
+
attempts: 0,
|
|
374
|
+
routing: {
|
|
375
|
+
complexity: "simple" as const,
|
|
376
|
+
modelTier: "fast" as const,
|
|
377
|
+
testStrategy: "test-after" as const,
|
|
378
|
+
reasoning: "simple",
|
|
379
|
+
},
|
|
380
|
+
}));
|
|
381
|
+
|
|
382
|
+
const batches = groupStoriesIntoBatches(stories, 2);
|
|
383
|
+
|
|
384
|
+
expect(batches).toHaveLength(3);
|
|
385
|
+
expect(batches[0].stories).toHaveLength(2);
|
|
386
|
+
expect(batches[1].stories).toHaveLength(2);
|
|
387
|
+
expect(batches[2].stories).toHaveLength(1);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("handles mixed complexity pattern", () => {
|
|
391
|
+
const stories: UserStory[] = [
|
|
392
|
+
{
|
|
393
|
+
id: "US-001",
|
|
394
|
+
title: "Simple 1",
|
|
395
|
+
description: "Simple",
|
|
396
|
+
acceptanceCriteria: ["AC1"],
|
|
397
|
+
tags: [],
|
|
398
|
+
dependencies: [],
|
|
399
|
+
status: "pending",
|
|
400
|
+
passes: false,
|
|
401
|
+
escalations: [],
|
|
402
|
+
attempts: 0,
|
|
403
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: "US-002",
|
|
407
|
+
title: "Complex",
|
|
408
|
+
description: "Complex",
|
|
409
|
+
acceptanceCriteria: ["AC2"],
|
|
410
|
+
tags: [],
|
|
411
|
+
dependencies: [],
|
|
412
|
+
status: "pending",
|
|
413
|
+
passes: false,
|
|
414
|
+
escalations: [],
|
|
415
|
+
attempts: 0,
|
|
416
|
+
routing: {
|
|
417
|
+
complexity: "complex",
|
|
418
|
+
modelTier: "balanced",
|
|
419
|
+
testStrategy: "three-session-tdd",
|
|
420
|
+
reasoning: "complex",
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "US-003",
|
|
425
|
+
title: "Simple 2",
|
|
426
|
+
description: "Simple",
|
|
427
|
+
acceptanceCriteria: ["AC3"],
|
|
428
|
+
tags: [],
|
|
429
|
+
dependencies: [],
|
|
430
|
+
status: "pending",
|
|
431
|
+
passes: false,
|
|
432
|
+
escalations: [],
|
|
433
|
+
attempts: 0,
|
|
434
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: "US-004",
|
|
438
|
+
title: "Simple 3",
|
|
439
|
+
description: "Simple",
|
|
440
|
+
acceptanceCriteria: ["AC4"],
|
|
441
|
+
tags: [],
|
|
442
|
+
dependencies: [],
|
|
443
|
+
status: "pending",
|
|
444
|
+
passes: false,
|
|
445
|
+
escalations: [],
|
|
446
|
+
attempts: 0,
|
|
447
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const batches = groupStoriesIntoBatches(stories);
|
|
452
|
+
|
|
453
|
+
expect(batches).toHaveLength(3);
|
|
454
|
+
// US-001 alone
|
|
455
|
+
expect(batches[0].isBatch).toBe(false);
|
|
456
|
+
expect(batches[0].stories).toHaveLength(1);
|
|
457
|
+
// US-002 alone
|
|
458
|
+
expect(batches[1].isBatch).toBe(false);
|
|
459
|
+
expect(batches[1].stories).toHaveLength(1);
|
|
460
|
+
// US-003 and US-004 batched
|
|
461
|
+
expect(batches[2].isBatch).toBe(true);
|
|
462
|
+
expect(batches[2].stories).toHaveLength(2);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("precomputeBatchPlan", () => {
|
|
467
|
+
test("precomputes batch plan from ready stories", () => {
|
|
468
|
+
const stories: UserStory[] = [
|
|
469
|
+
{
|
|
470
|
+
id: "US-001",
|
|
471
|
+
title: "Simple 1",
|
|
472
|
+
description: "First simple",
|
|
473
|
+
acceptanceCriteria: ["AC1"],
|
|
474
|
+
tags: [],
|
|
475
|
+
dependencies: [],
|
|
476
|
+
status: "pending",
|
|
477
|
+
passes: false,
|
|
478
|
+
escalations: [],
|
|
479
|
+
attempts: 0,
|
|
480
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "US-002",
|
|
484
|
+
title: "Simple 2",
|
|
485
|
+
description: "Second simple",
|
|
486
|
+
acceptanceCriteria: ["AC2"],
|
|
487
|
+
tags: [],
|
|
488
|
+
dependencies: [],
|
|
489
|
+
status: "pending",
|
|
490
|
+
passes: false,
|
|
491
|
+
escalations: [],
|
|
492
|
+
attempts: 0,
|
|
493
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "US-003",
|
|
497
|
+
title: "Complex",
|
|
498
|
+
description: "Complex story",
|
|
499
|
+
acceptanceCriteria: ["AC3"],
|
|
500
|
+
tags: [],
|
|
501
|
+
dependencies: [],
|
|
502
|
+
status: "pending",
|
|
503
|
+
passes: false,
|
|
504
|
+
escalations: [],
|
|
505
|
+
attempts: 0,
|
|
506
|
+
routing: {
|
|
507
|
+
complexity: "complex",
|
|
508
|
+
modelTier: "balanced",
|
|
509
|
+
testStrategy: "three-session-tdd",
|
|
510
|
+
reasoning: "complex",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const plan = precomputeBatchPlan(stories);
|
|
516
|
+
|
|
517
|
+
expect(plan).toHaveLength(2);
|
|
518
|
+
// First batch: 2 simple stories
|
|
519
|
+
expect(plan[0].stories).toHaveLength(2);
|
|
520
|
+
expect(plan[0].isBatch).toBe(true);
|
|
521
|
+
expect(plan[0].stories.map((s) => s.id)).toEqual(["US-001", "US-002"]);
|
|
522
|
+
// Second batch: 1 complex story
|
|
523
|
+
expect(plan[1].stories).toHaveLength(1);
|
|
524
|
+
expect(plan[1].isBatch).toBe(false);
|
|
525
|
+
expect(plan[1].stories[0].id).toBe("US-003");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("maintains story order from PRD", () => {
|
|
529
|
+
const stories: UserStory[] = [
|
|
530
|
+
{
|
|
531
|
+
id: "US-001",
|
|
532
|
+
title: "Simple 1",
|
|
533
|
+
description: "First",
|
|
534
|
+
acceptanceCriteria: ["AC1"],
|
|
535
|
+
tags: [],
|
|
536
|
+
dependencies: [],
|
|
537
|
+
status: "pending",
|
|
538
|
+
passes: false,
|
|
539
|
+
escalations: [],
|
|
540
|
+
attempts: 0,
|
|
541
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: "US-002",
|
|
545
|
+
title: "Complex",
|
|
546
|
+
description: "Middle",
|
|
547
|
+
acceptanceCriteria: ["AC2"],
|
|
548
|
+
tags: [],
|
|
549
|
+
dependencies: [],
|
|
550
|
+
status: "pending",
|
|
551
|
+
passes: false,
|
|
552
|
+
escalations: [],
|
|
553
|
+
attempts: 0,
|
|
554
|
+
routing: { complexity: "medium", modelTier: "balanced", testStrategy: "test-after", reasoning: "medium" },
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: "US-003",
|
|
558
|
+
title: "Simple 2",
|
|
559
|
+
description: "Last",
|
|
560
|
+
acceptanceCriteria: ["AC3"],
|
|
561
|
+
tags: [],
|
|
562
|
+
dependencies: [],
|
|
563
|
+
status: "pending",
|
|
564
|
+
passes: false,
|
|
565
|
+
escalations: [],
|
|
566
|
+
attempts: 0,
|
|
567
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
568
|
+
},
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
const plan = precomputeBatchPlan(stories);
|
|
572
|
+
|
|
573
|
+
// Should maintain order: US-001, US-002, US-003
|
|
574
|
+
expect(plan).toHaveLength(3);
|
|
575
|
+
expect(plan[0].stories[0].id).toBe("US-001");
|
|
576
|
+
expect(plan[1].stories[0].id).toBe("US-002");
|
|
577
|
+
expect(plan[2].stories[0].id).toBe("US-003");
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("only batches simple stories with test-after strategy", () => {
|
|
581
|
+
const stories: UserStory[] = [
|
|
582
|
+
{
|
|
583
|
+
id: "US-001",
|
|
584
|
+
title: "Simple TDD",
|
|
585
|
+
description: "Simple but uses TDD",
|
|
586
|
+
acceptanceCriteria: ["AC1"],
|
|
587
|
+
tags: [],
|
|
588
|
+
dependencies: [],
|
|
589
|
+
status: "pending",
|
|
590
|
+
passes: false,
|
|
591
|
+
escalations: [],
|
|
592
|
+
attempts: 0,
|
|
593
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "three-session-tdd", reasoning: "simple" },
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
id: "US-002",
|
|
597
|
+
title: "Simple test-after",
|
|
598
|
+
description: "Simple with test-after",
|
|
599
|
+
acceptanceCriteria: ["AC2"],
|
|
600
|
+
tags: [],
|
|
601
|
+
dependencies: [],
|
|
602
|
+
status: "pending",
|
|
603
|
+
passes: false,
|
|
604
|
+
escalations: [],
|
|
605
|
+
attempts: 0,
|
|
606
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
const plan = precomputeBatchPlan(stories);
|
|
611
|
+
|
|
612
|
+
// US-001 should be individual (TDD), US-002 should be individual (no other simple test-after to batch with)
|
|
613
|
+
expect(plan).toHaveLength(2);
|
|
614
|
+
expect(plan[0].isBatch).toBe(false);
|
|
615
|
+
expect(plan[0].stories[0].id).toBe("US-001");
|
|
616
|
+
expect(plan[1].isBatch).toBe(false);
|
|
617
|
+
expect(plan[1].stories[0].id).toBe("US-002");
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("handles empty story list", () => {
|
|
621
|
+
const stories: UserStory[] = [];
|
|
622
|
+
const plan = precomputeBatchPlan(stories);
|
|
623
|
+
expect(plan).toHaveLength(0);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("respects max batch size", () => {
|
|
627
|
+
const stories: UserStory[] = Array.from({ length: 6 }, (_, i) => ({
|
|
628
|
+
id: `US-00${i + 1}`,
|
|
629
|
+
title: `Simple ${i + 1}`,
|
|
630
|
+
description: `Story ${i + 1}`,
|
|
631
|
+
acceptanceCriteria: [`AC${i + 1}`],
|
|
632
|
+
tags: [],
|
|
633
|
+
dependencies: [],
|
|
634
|
+
status: "pending" as const,
|
|
635
|
+
passes: false,
|
|
636
|
+
escalations: [],
|
|
637
|
+
attempts: 0,
|
|
638
|
+
routing: {
|
|
639
|
+
complexity: "simple" as const,
|
|
640
|
+
modelTier: "fast" as const,
|
|
641
|
+
testStrategy: "test-after" as const,
|
|
642
|
+
reasoning: "simple",
|
|
643
|
+
},
|
|
644
|
+
}));
|
|
645
|
+
|
|
646
|
+
const plan = precomputeBatchPlan(stories, 3);
|
|
647
|
+
|
|
648
|
+
// Should create 2 batches of 3
|
|
649
|
+
expect(plan).toHaveLength(2);
|
|
650
|
+
expect(plan[0].stories).toHaveLength(3);
|
|
651
|
+
expect(plan[0].isBatch).toBe(true);
|
|
652
|
+
expect(plan[1].stories).toHaveLength(3);
|
|
653
|
+
expect(plan[1].isBatch).toBe(true);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("handles all stories already passed", () => {
|
|
657
|
+
const stories: UserStory[] = [
|
|
658
|
+
{
|
|
659
|
+
id: "US-001",
|
|
660
|
+
title: "Passed",
|
|
661
|
+
description: "Already done",
|
|
662
|
+
acceptanceCriteria: ["AC1"],
|
|
663
|
+
tags: [],
|
|
664
|
+
dependencies: [],
|
|
665
|
+
status: "passed",
|
|
666
|
+
passes: true,
|
|
667
|
+
escalations: [],
|
|
668
|
+
attempts: 1,
|
|
669
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
670
|
+
},
|
|
671
|
+
];
|
|
672
|
+
|
|
673
|
+
const plan = precomputeBatchPlan(stories);
|
|
674
|
+
|
|
675
|
+
// Should still include passed story in plan (filtering happens at runtime)
|
|
676
|
+
expect(plan).toHaveLength(1);
|
|
677
|
+
expect(plan[0].stories[0].id).toBe("US-001");
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe("Batch Failure Escalation Strategy", () => {
|
|
682
|
+
test("batch failure should escalate only first story, others remain at same tier", () => {
|
|
683
|
+
// Simulate a batch of 4 simple stories at 'fast' tier
|
|
684
|
+
const batchStories: UserStory[] = [
|
|
685
|
+
{
|
|
686
|
+
id: "US-001",
|
|
687
|
+
title: "Simple 1",
|
|
688
|
+
description: "First story in batch",
|
|
689
|
+
acceptanceCriteria: ["AC1"],
|
|
690
|
+
tags: [],
|
|
691
|
+
dependencies: [],
|
|
692
|
+
status: "pending",
|
|
693
|
+
passes: false,
|
|
694
|
+
escalations: [],
|
|
695
|
+
attempts: 0,
|
|
696
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: "US-002",
|
|
700
|
+
title: "Simple 2",
|
|
701
|
+
description: "Second story in batch",
|
|
702
|
+
acceptanceCriteria: ["AC2"],
|
|
703
|
+
tags: [],
|
|
704
|
+
dependencies: [],
|
|
705
|
+
status: "pending",
|
|
706
|
+
passes: false,
|
|
707
|
+
escalations: [],
|
|
708
|
+
attempts: 0,
|
|
709
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
id: "US-003",
|
|
713
|
+
title: "Simple 3",
|
|
714
|
+
description: "Third story in batch",
|
|
715
|
+
acceptanceCriteria: ["AC3"],
|
|
716
|
+
tags: [],
|
|
717
|
+
dependencies: [],
|
|
718
|
+
status: "pending",
|
|
719
|
+
passes: false,
|
|
720
|
+
escalations: [],
|
|
721
|
+
attempts: 0,
|
|
722
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
id: "US-004",
|
|
726
|
+
title: "Simple 4",
|
|
727
|
+
description: "Fourth story in batch",
|
|
728
|
+
acceptanceCriteria: ["AC4"],
|
|
729
|
+
tags: [],
|
|
730
|
+
dependencies: [],
|
|
731
|
+
status: "pending",
|
|
732
|
+
passes: false,
|
|
733
|
+
escalations: [],
|
|
734
|
+
attempts: 0,
|
|
735
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
736
|
+
},
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
// When batch fails at 'fast' tier:
|
|
740
|
+
// 1. First story (US-001) should escalate to 'balanced'
|
|
741
|
+
const firstStory = batchStories[0];
|
|
742
|
+
const currentTier = firstStory.routing!.modelTier;
|
|
743
|
+
const tierOrder = [
|
|
744
|
+
{ tier: "fast", attempts: 5 },
|
|
745
|
+
{ tier: "balanced", attempts: 3 },
|
|
746
|
+
{ tier: "powerful", attempts: 2 },
|
|
747
|
+
];
|
|
748
|
+
const nextTier = escalateTier(currentTier!, tierOrder);
|
|
749
|
+
|
|
750
|
+
expect(currentTier).toBe("fast");
|
|
751
|
+
expect(nextTier).toBe("balanced");
|
|
752
|
+
|
|
753
|
+
// 2. Remaining stories (US-002, US-003, US-004) should remain at 'fast' tier
|
|
754
|
+
// They will be retried individually at the same tier on next iteration
|
|
755
|
+
const remainingStories = batchStories.slice(1);
|
|
756
|
+
for (const story of remainingStories) {
|
|
757
|
+
expect(story.routing!.modelTier).toBe("fast");
|
|
758
|
+
expect(story.status).toBe("pending");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// 3. This tests the documented "Option B" strategy:
|
|
762
|
+
// - Only first story escalates
|
|
763
|
+
// - Others retry individually at same tier first
|
|
764
|
+
// - This minimizes cost and provides better error isolation
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("batch failure escalation follows standard escalation chain", () => {
|
|
768
|
+
const tierOrder = [
|
|
769
|
+
{ tier: "fast", attempts: 5 },
|
|
770
|
+
{ tier: "balanced", attempts: 3 },
|
|
771
|
+
{ tier: "powerful", attempts: 2 },
|
|
772
|
+
];
|
|
773
|
+
const tiers = ["fast", "balanced", "powerful"];
|
|
774
|
+
const expectedNext = ["balanced", "powerful", null];
|
|
775
|
+
|
|
776
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
777
|
+
const nextTier = escalateTier(tiers[i], tierOrder);
|
|
778
|
+
expect(nextTier).toBe(expectedNext[i]);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const powerfulTier = escalateTier("powerful", tierOrder);
|
|
782
|
+
expect(powerfulTier).toBeNull();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("batch failure with max attempts should not escalate", () => {
|
|
786
|
+
// When first story in batch has already hit max attempts (e.g., 3),
|
|
787
|
+
// it should be marked as failed instead of escalated
|
|
788
|
+
const story: UserStory = {
|
|
789
|
+
id: "US-001",
|
|
790
|
+
title: "Simple with max attempts",
|
|
791
|
+
description: "Story that has already been retried 3 times",
|
|
792
|
+
acceptanceCriteria: ["AC1"],
|
|
793
|
+
tags: [],
|
|
794
|
+
dependencies: [],
|
|
795
|
+
status: "pending",
|
|
796
|
+
passes: false,
|
|
797
|
+
escalations: [],
|
|
798
|
+
attempts: 3, // Already at max attempts (typical config.autoMode.escalation.maxAttempts = 3)
|
|
799
|
+
routing: { complexity: "simple", modelTier: "balanced", testStrategy: "test-after", reasoning: "simple" },
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
const maxAttempts = 3;
|
|
803
|
+
const escalationEnabled = true;
|
|
804
|
+
|
|
805
|
+
// Should not escalate if attempts >= maxAttempts
|
|
806
|
+
if (escalationEnabled && story.attempts < maxAttempts) {
|
|
807
|
+
// This branch should NOT be taken
|
|
808
|
+
expect(false).toBe(true); // Should not reach here
|
|
809
|
+
} else {
|
|
810
|
+
// Story should be marked as failed (not escalated)
|
|
811
|
+
expect(story.attempts).toBeGreaterThanOrEqual(maxAttempts);
|
|
812
|
+
// In actual runner code, markStoryFailed() would be called here
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
describe("Queue Commands Before Batch Execution", () => {
|
|
818
|
+
test("SKIP command should filter story from batch before execution", () => {
|
|
819
|
+
// Simulate a batch of 3 simple stories: [US-001, US-002, US-003]
|
|
820
|
+
// User issues SKIP US-002 in .queue.txt
|
|
821
|
+
// Expected: Batch should only contain [US-001, US-003]
|
|
822
|
+
const batchStories: UserStory[] = [
|
|
823
|
+
{
|
|
824
|
+
id: "US-001",
|
|
825
|
+
title: "Simple 1",
|
|
826
|
+
description: "First story in batch",
|
|
827
|
+
acceptanceCriteria: ["AC1"],
|
|
828
|
+
tags: [],
|
|
829
|
+
dependencies: [],
|
|
830
|
+
status: "pending",
|
|
831
|
+
passes: false,
|
|
832
|
+
escalations: [],
|
|
833
|
+
attempts: 0,
|
|
834
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
id: "US-002",
|
|
838
|
+
title: "Simple 2",
|
|
839
|
+
description: "Second story in batch (to be skipped)",
|
|
840
|
+
acceptanceCriteria: ["AC2"],
|
|
841
|
+
tags: [],
|
|
842
|
+
dependencies: [],
|
|
843
|
+
status: "pending",
|
|
844
|
+
passes: false,
|
|
845
|
+
escalations: [],
|
|
846
|
+
attempts: 0,
|
|
847
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
id: "US-003",
|
|
851
|
+
title: "Simple 3",
|
|
852
|
+
description: "Third story in batch",
|
|
853
|
+
acceptanceCriteria: ["AC3"],
|
|
854
|
+
tags: [],
|
|
855
|
+
dependencies: [],
|
|
856
|
+
status: "pending",
|
|
857
|
+
passes: false,
|
|
858
|
+
escalations: [],
|
|
859
|
+
attempts: 0,
|
|
860
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
861
|
+
},
|
|
862
|
+
];
|
|
863
|
+
|
|
864
|
+
// Simulate SKIP command processing
|
|
865
|
+
const skipCommand = { type: "SKIP" as const, storyId: "US-002" };
|
|
866
|
+
const storyIndex = batchStories.findIndex((s) => s.id === skipCommand.storyId);
|
|
867
|
+
|
|
868
|
+
expect(storyIndex).toBe(1);
|
|
869
|
+
|
|
870
|
+
// Remove from batch
|
|
871
|
+
const filteredBatch = batchStories.filter((s) => s.id !== skipCommand.storyId);
|
|
872
|
+
|
|
873
|
+
expect(filteredBatch).toHaveLength(2);
|
|
874
|
+
expect(filteredBatch.map((s) => s.id)).toEqual(["US-001", "US-003"]);
|
|
875
|
+
expect(filteredBatch.every((s) => s.status === "pending")).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("SKIP all stories in batch should result in empty batch and continue to next iteration", () => {
|
|
879
|
+
// Simulate a batch of 2 simple stories: [US-001, US-002]
|
|
880
|
+
// User issues SKIP US-001 and SKIP US-002
|
|
881
|
+
// Expected: Batch becomes empty, runner should continue to next iteration
|
|
882
|
+
const batchStories: UserStory[] = [
|
|
883
|
+
{
|
|
884
|
+
id: "US-001",
|
|
885
|
+
title: "Simple 1",
|
|
886
|
+
description: "First story",
|
|
887
|
+
acceptanceCriteria: ["AC1"],
|
|
888
|
+
tags: [],
|
|
889
|
+
dependencies: [],
|
|
890
|
+
status: "pending",
|
|
891
|
+
passes: false,
|
|
892
|
+
escalations: [],
|
|
893
|
+
attempts: 0,
|
|
894
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: "US-002",
|
|
898
|
+
title: "Simple 2",
|
|
899
|
+
description: "Second story",
|
|
900
|
+
acceptanceCriteria: ["AC2"],
|
|
901
|
+
tags: [],
|
|
902
|
+
dependencies: [],
|
|
903
|
+
status: "pending",
|
|
904
|
+
passes: false,
|
|
905
|
+
escalations: [],
|
|
906
|
+
attempts: 0,
|
|
907
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
908
|
+
},
|
|
909
|
+
];
|
|
910
|
+
|
|
911
|
+
// Simulate SKIP commands
|
|
912
|
+
const skipCommands = [
|
|
913
|
+
{ type: "SKIP" as const, storyId: "US-001" },
|
|
914
|
+
{ type: "SKIP" as const, storyId: "US-002" },
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
let filteredBatch = [...batchStories];
|
|
918
|
+
for (const cmd of skipCommands) {
|
|
919
|
+
filteredBatch = filteredBatch.filter((s) => s.id !== cmd.storyId);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
expect(filteredBatch).toHaveLength(0);
|
|
923
|
+
// When batch is empty, runner should continue to next iteration
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("PAUSE command should halt execution before batch starts", () => {
|
|
927
|
+
// When PAUSE is issued before batch execution
|
|
928
|
+
// Expected: Execution stops, no stories are processed
|
|
929
|
+
const pauseCommand = "PAUSE" as const;
|
|
930
|
+
const batchStories: UserStory[] = [
|
|
931
|
+
{
|
|
932
|
+
id: "US-001",
|
|
933
|
+
title: "Simple 1",
|
|
934
|
+
description: "First story",
|
|
935
|
+
acceptanceCriteria: ["AC1"],
|
|
936
|
+
tags: [],
|
|
937
|
+
dependencies: [],
|
|
938
|
+
status: "pending",
|
|
939
|
+
passes: false,
|
|
940
|
+
escalations: [],
|
|
941
|
+
attempts: 0,
|
|
942
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
943
|
+
},
|
|
944
|
+
];
|
|
945
|
+
|
|
946
|
+
// Simulate PAUSE processing
|
|
947
|
+
let shouldHalt = false;
|
|
948
|
+
if (pauseCommand === "PAUSE") {
|
|
949
|
+
shouldHalt = true;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
expect(shouldHalt).toBe(true);
|
|
953
|
+
// When halted, no stories should be executed
|
|
954
|
+
expect(batchStories.every((s) => s.status === "pending")).toBe(true);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test("SKIP command for story not in batch should still mark it as skipped", () => {
|
|
958
|
+
// Simulate batch [US-001, US-002] but user issues SKIP US-003
|
|
959
|
+
// Expected: US-003 is marked skipped even though not in current batch
|
|
960
|
+
const batchStories: UserStory[] = [
|
|
961
|
+
{
|
|
962
|
+
id: "US-001",
|
|
963
|
+
title: "Simple 1",
|
|
964
|
+
description: "First story",
|
|
965
|
+
acceptanceCriteria: ["AC1"],
|
|
966
|
+
tags: [],
|
|
967
|
+
dependencies: [],
|
|
968
|
+
status: "pending",
|
|
969
|
+
passes: false,
|
|
970
|
+
escalations: [],
|
|
971
|
+
attempts: 0,
|
|
972
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
id: "US-002",
|
|
976
|
+
title: "Simple 2",
|
|
977
|
+
description: "Second story",
|
|
978
|
+
acceptanceCriteria: ["AC2"],
|
|
979
|
+
tags: [],
|
|
980
|
+
dependencies: [],
|
|
981
|
+
status: "pending",
|
|
982
|
+
passes: false,
|
|
983
|
+
escalations: [],
|
|
984
|
+
attempts: 0,
|
|
985
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
986
|
+
},
|
|
987
|
+
];
|
|
988
|
+
|
|
989
|
+
const skipCommand = { type: "SKIP" as const, storyId: "US-003" };
|
|
990
|
+
const storyIndex = batchStories.findIndex((s) => s.id === skipCommand.storyId);
|
|
991
|
+
|
|
992
|
+
// Story not found in batch
|
|
993
|
+
expect(storyIndex).toBe(-1);
|
|
994
|
+
|
|
995
|
+
// But should still be processed (in actual runner, PRD would be checked)
|
|
996
|
+
// This test validates the logic path exists
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test("batch size reduction from 4 to 1 should disable batch execution flag", () => {
|
|
1000
|
+
// Simulate batch [US-001, US-002, US-003, US-004]
|
|
1001
|
+
// User issues SKIP US-002, SKIP US-003, SKIP US-004
|
|
1002
|
+
// Expected: Only US-001 remains, isBatchExecution should be false
|
|
1003
|
+
const batchStories: UserStory[] = [
|
|
1004
|
+
{
|
|
1005
|
+
id: "US-001",
|
|
1006
|
+
title: "Simple 1",
|
|
1007
|
+
description: "First story",
|
|
1008
|
+
acceptanceCriteria: ["AC1"],
|
|
1009
|
+
tags: [],
|
|
1010
|
+
dependencies: [],
|
|
1011
|
+
status: "pending",
|
|
1012
|
+
passes: false,
|
|
1013
|
+
escalations: [],
|
|
1014
|
+
attempts: 0,
|
|
1015
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
id: "US-002",
|
|
1019
|
+
title: "Simple 2",
|
|
1020
|
+
description: "Second story",
|
|
1021
|
+
acceptanceCriteria: ["AC2"],
|
|
1022
|
+
tags: [],
|
|
1023
|
+
dependencies: [],
|
|
1024
|
+
status: "pending",
|
|
1025
|
+
passes: false,
|
|
1026
|
+
escalations: [],
|
|
1027
|
+
attempts: 0,
|
|
1028
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
id: "US-003",
|
|
1032
|
+
title: "Simple 3",
|
|
1033
|
+
description: "Third story",
|
|
1034
|
+
acceptanceCriteria: ["AC3"],
|
|
1035
|
+
tags: [],
|
|
1036
|
+
dependencies: [],
|
|
1037
|
+
status: "pending",
|
|
1038
|
+
passes: false,
|
|
1039
|
+
escalations: [],
|
|
1040
|
+
attempts: 0,
|
|
1041
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
id: "US-004",
|
|
1045
|
+
title: "Simple 4",
|
|
1046
|
+
description: "Fourth story",
|
|
1047
|
+
acceptanceCriteria: ["AC4"],
|
|
1048
|
+
tags: [],
|
|
1049
|
+
dependencies: [],
|
|
1050
|
+
status: "pending",
|
|
1051
|
+
passes: false,
|
|
1052
|
+
escalations: [],
|
|
1053
|
+
attempts: 0,
|
|
1054
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1055
|
+
},
|
|
1056
|
+
];
|
|
1057
|
+
|
|
1058
|
+
let isBatchExecution = true; // Initially true for 4 stories
|
|
1059
|
+
const skipCommands = [
|
|
1060
|
+
{ type: "SKIP" as const, storyId: "US-002" },
|
|
1061
|
+
{ type: "SKIP" as const, storyId: "US-003" },
|
|
1062
|
+
{ type: "SKIP" as const, storyId: "US-004" },
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
let filteredBatch = [...batchStories];
|
|
1066
|
+
for (const cmd of skipCommands) {
|
|
1067
|
+
filteredBatch = filteredBatch.filter((s) => s.id !== cmd.storyId);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Re-check batch flag
|
|
1071
|
+
if (isBatchExecution && filteredBatch.length === 1) {
|
|
1072
|
+
isBatchExecution = false;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
expect(filteredBatch).toHaveLength(1);
|
|
1076
|
+
expect(filteredBatch[0].id).toBe("US-001");
|
|
1077
|
+
expect(isBatchExecution).toBe(false);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
test("ABORT command should mark all pending stories as skipped", () => {
|
|
1081
|
+
// When ABORT is issued
|
|
1082
|
+
// Expected: All pending stories should be marked as skipped
|
|
1083
|
+
const abortCommand = "ABORT" as const;
|
|
1084
|
+
const allStories: UserStory[] = [
|
|
1085
|
+
{
|
|
1086
|
+
id: "US-001",
|
|
1087
|
+
title: "Passed",
|
|
1088
|
+
description: "Already done",
|
|
1089
|
+
acceptanceCriteria: ["AC1"],
|
|
1090
|
+
tags: [],
|
|
1091
|
+
dependencies: [],
|
|
1092
|
+
status: "passed",
|
|
1093
|
+
passes: true,
|
|
1094
|
+
escalations: [],
|
|
1095
|
+
attempts: 1,
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
id: "US-002",
|
|
1099
|
+
title: "Pending 1",
|
|
1100
|
+
description: "Not started",
|
|
1101
|
+
acceptanceCriteria: ["AC2"],
|
|
1102
|
+
tags: [],
|
|
1103
|
+
dependencies: [],
|
|
1104
|
+
status: "pending",
|
|
1105
|
+
passes: false,
|
|
1106
|
+
escalations: [],
|
|
1107
|
+
attempts: 0,
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
id: "US-003",
|
|
1111
|
+
title: "Pending 2",
|
|
1112
|
+
description: "Not started",
|
|
1113
|
+
acceptanceCriteria: ["AC3"],
|
|
1114
|
+
tags: [],
|
|
1115
|
+
dependencies: [],
|
|
1116
|
+
status: "pending",
|
|
1117
|
+
passes: false,
|
|
1118
|
+
escalations: [],
|
|
1119
|
+
attempts: 0,
|
|
1120
|
+
},
|
|
1121
|
+
];
|
|
1122
|
+
|
|
1123
|
+
// Simulate ABORT processing
|
|
1124
|
+
let shouldAbort = false;
|
|
1125
|
+
if (abortCommand === "ABORT") {
|
|
1126
|
+
shouldAbort = true;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
expect(shouldAbort).toBe(true);
|
|
1130
|
+
|
|
1131
|
+
// Filter pending stories that should be skipped
|
|
1132
|
+
const pendingStories = allStories.filter((s) => s.status === "pending");
|
|
1133
|
+
expect(pendingStories).toHaveLength(2);
|
|
1134
|
+
expect(pendingStories.map((s) => s.id)).toEqual(["US-002", "US-003"]);
|
|
1135
|
+
|
|
1136
|
+
// In actual runner, these would be marked as skipped via markStorySkipped()
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
describe("Configurable Escalation Chain (ADR-003)", () => {
|
|
1141
|
+
const defaultTiers = [
|
|
1142
|
+
{ tier: "fast", attempts: 5 },
|
|
1143
|
+
{ tier: "balanced", attempts: 3 },
|
|
1144
|
+
{ tier: "powerful", attempts: 2 },
|
|
1145
|
+
];
|
|
1146
|
+
|
|
1147
|
+
test("escalateTier with standard chain", () => {
|
|
1148
|
+
expect(escalateTier("fast", defaultTiers)).toBe("balanced");
|
|
1149
|
+
expect(escalateTier("balanced", defaultTiers)).toBe("powerful");
|
|
1150
|
+
expect(escalateTier("powerful", defaultTiers)).toBeNull();
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test("escalateTier with custom tierOrder (skip balanced)", () => {
|
|
1154
|
+
const customOrder = [
|
|
1155
|
+
{ tier: "fast", attempts: 5 },
|
|
1156
|
+
{ tier: "powerful", attempts: 2 },
|
|
1157
|
+
];
|
|
1158
|
+
expect(escalateTier("fast", customOrder)).toBe("powerful");
|
|
1159
|
+
expect(escalateTier("powerful", customOrder)).toBeNull();
|
|
1160
|
+
expect(escalateTier("balanced", customOrder)).toBeNull();
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
test("escalateTier with single-tier order", () => {
|
|
1164
|
+
const singleTier = [{ tier: "fast", attempts: 10 }];
|
|
1165
|
+
expect(escalateTier("fast", singleTier)).toBeNull();
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test("escalateTier with reversed order", () => {
|
|
1169
|
+
const reversed = [
|
|
1170
|
+
{ tier: "powerful", attempts: 2 },
|
|
1171
|
+
{ tier: "balanced", attempts: 3 },
|
|
1172
|
+
{ tier: "fast", attempts: 5 },
|
|
1173
|
+
];
|
|
1174
|
+
expect(escalateTier("powerful", reversed)).toBe("balanced");
|
|
1175
|
+
expect(escalateTier("balanced", reversed)).toBe("fast");
|
|
1176
|
+
expect(escalateTier("fast", reversed)).toBeNull();
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
test("escalateTier with empty tierOrder returns null", () => {
|
|
1180
|
+
expect(escalateTier("fast", [])).toBeNull();
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("escalateTier with three-tier standard order", () => {
|
|
1184
|
+
expect(escalateTier("fast", defaultTiers)).toBe("balanced");
|
|
1185
|
+
expect(escalateTier("balanced", defaultTiers)).toBe("powerful");
|
|
1186
|
+
expect(escalateTier("powerful", defaultTiers)).toBeNull();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
test("escalateTier should return null for unknown tier", () => {
|
|
1190
|
+
expect(escalateTier("unknown", defaultTiers)).toBeNull();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
test("escalateTier should be idempotent at max tier", () => {
|
|
1194
|
+
expect(escalateTier("powerful", defaultTiers)).toBeNull();
|
|
1195
|
+
// Call again — still null
|
|
1196
|
+
expect(escalateTier("powerful", defaultTiers)).toBeNull();
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test("calculateMaxIterations sums all tier attempts", () => {
|
|
1200
|
+
const { calculateMaxIterations } = require("../../src/execution/escalation");
|
|
1201
|
+
expect(calculateMaxIterations(defaultTiers)).toBe(10); // 5+3+2
|
|
1202
|
+
expect(calculateMaxIterations([{ tier: "fast", attempts: 1 }])).toBe(1);
|
|
1203
|
+
expect(calculateMaxIterations([])).toBe(0);
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
describe("Pre-Iteration Escalation (BUG-16, BUG-17)", () => {
|
|
1208
|
+
const defaultTiers = [
|
|
1209
|
+
{ tier: "fast", attempts: 5 },
|
|
1210
|
+
{ tier: "balanced", attempts: 3 },
|
|
1211
|
+
{ tier: "powerful", attempts: 2 },
|
|
1212
|
+
];
|
|
1213
|
+
|
|
1214
|
+
test("story with attempts >= tier budget should trigger escalation before agent spawn", () => {
|
|
1215
|
+
// Simulate a story at "fast" tier with 5 attempts (budget exhausted)
|
|
1216
|
+
const story: UserStory = {
|
|
1217
|
+
id: "US-001",
|
|
1218
|
+
title: "Test story",
|
|
1219
|
+
description: "Test",
|
|
1220
|
+
acceptanceCriteria: ["AC1"],
|
|
1221
|
+
tags: [],
|
|
1222
|
+
dependencies: [],
|
|
1223
|
+
status: "pending",
|
|
1224
|
+
passes: false,
|
|
1225
|
+
escalations: [],
|
|
1226
|
+
attempts: 5, // Exhausted fast tier budget (5 attempts)
|
|
1227
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// Get tier config
|
|
1231
|
+
const currentTier = story.routing!.modelTier;
|
|
1232
|
+
const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
|
|
1233
|
+
|
|
1234
|
+
expect(tierCfg).toBeDefined();
|
|
1235
|
+
expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
|
|
1236
|
+
|
|
1237
|
+
// Should escalate to next tier
|
|
1238
|
+
const nextTier = escalateTier(currentTier!, defaultTiers);
|
|
1239
|
+
expect(nextTier).toBe("balanced");
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
test("story at balanced tier with 3 attempts should escalate to powerful", () => {
|
|
1243
|
+
const story: UserStory = {
|
|
1244
|
+
id: "US-002",
|
|
1245
|
+
title: "Test story",
|
|
1246
|
+
description: "Test",
|
|
1247
|
+
acceptanceCriteria: ["AC1"],
|
|
1248
|
+
tags: [],
|
|
1249
|
+
dependencies: [],
|
|
1250
|
+
status: "pending",
|
|
1251
|
+
passes: false,
|
|
1252
|
+
escalations: [],
|
|
1253
|
+
attempts: 3, // Exhausted balanced tier budget (3 attempts)
|
|
1254
|
+
routing: { complexity: "medium", modelTier: "balanced", testStrategy: "test-after", reasoning: "medium" },
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const currentTier = story.routing!.modelTier;
|
|
1258
|
+
const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
|
|
1259
|
+
|
|
1260
|
+
expect(tierCfg).toBeDefined();
|
|
1261
|
+
expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
|
|
1262
|
+
|
|
1263
|
+
const nextTier = escalateTier(currentTier!, defaultTiers);
|
|
1264
|
+
expect(nextTier).toBe("powerful");
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test("story at powerful tier with 2 attempts should mark as FAILED (no more tiers)", () => {
|
|
1268
|
+
const story: UserStory = {
|
|
1269
|
+
id: "US-003",
|
|
1270
|
+
title: "Test story",
|
|
1271
|
+
description: "Test",
|
|
1272
|
+
acceptanceCriteria: ["AC1"],
|
|
1273
|
+
tags: [],
|
|
1274
|
+
dependencies: [],
|
|
1275
|
+
status: "pending",
|
|
1276
|
+
passes: false,
|
|
1277
|
+
escalations: [],
|
|
1278
|
+
attempts: 2, // Exhausted powerful tier budget (2 attempts)
|
|
1279
|
+
routing: { complexity: "complex", modelTier: "powerful", testStrategy: "test-after", reasoning: "complex" },
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
const currentTier = story.routing!.modelTier;
|
|
1283
|
+
const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
|
|
1284
|
+
|
|
1285
|
+
expect(tierCfg).toBeDefined();
|
|
1286
|
+
expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
|
|
1287
|
+
|
|
1288
|
+
// No next tier available
|
|
1289
|
+
const nextTier = escalateTier(currentTier!, defaultTiers);
|
|
1290
|
+
expect(nextTier).toBeNull();
|
|
1291
|
+
|
|
1292
|
+
// Story should be marked as FAILED (not retried)
|
|
1293
|
+
// In actual runner code, markStoryFailed() would be called here
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
test("pre-iteration check prevents infinite loop at same tier", () => {
|
|
1297
|
+
// BUG-16: Stories were looping indefinitely at same tier
|
|
1298
|
+
// This test verifies that pre-iteration escalation prevents this
|
|
1299
|
+
|
|
1300
|
+
const story: UserStory = {
|
|
1301
|
+
id: "US-004",
|
|
1302
|
+
title: "ASSET_CHECK failing story",
|
|
1303
|
+
description: "Story with missing files",
|
|
1304
|
+
acceptanceCriteria: ["AC1"],
|
|
1305
|
+
tags: [],
|
|
1306
|
+
dependencies: [],
|
|
1307
|
+
status: "pending",
|
|
1308
|
+
passes: false,
|
|
1309
|
+
escalations: [],
|
|
1310
|
+
attempts: 5, // Budget exhausted
|
|
1311
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1312
|
+
priorErrors: ["ASSET_CHECK_FAILED: Missing file src/test.ts"],
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
// Pre-iteration check should trigger escalation
|
|
1316
|
+
const currentTier = story.routing!.modelTier;
|
|
1317
|
+
const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
|
|
1318
|
+
|
|
1319
|
+
expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
|
|
1320
|
+
|
|
1321
|
+
// Should escalate instead of retrying at same tier
|
|
1322
|
+
const nextTier = escalateTier(currentTier!, defaultTiers);
|
|
1323
|
+
expect(nextTier).toBe("balanced");
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
test("ASSET_CHECK failure should increment attempts and respect escalation", () => {
|
|
1327
|
+
// BUG-17: ASSET_CHECK failures were reverting to pending without escalation
|
|
1328
|
+
const story: UserStory = {
|
|
1329
|
+
id: "US-005",
|
|
1330
|
+
title: "Story with ASSET_CHECK failure",
|
|
1331
|
+
description: "Test",
|
|
1332
|
+
acceptanceCriteria: ["AC1"],
|
|
1333
|
+
tags: [],
|
|
1334
|
+
dependencies: [],
|
|
1335
|
+
status: "pending",
|
|
1336
|
+
passes: false,
|
|
1337
|
+
escalations: [],
|
|
1338
|
+
attempts: 4, // One attempt left in fast tier
|
|
1339
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
// Simulate ASSET_CHECK failure
|
|
1343
|
+
const updatedStory = {
|
|
1344
|
+
...story,
|
|
1345
|
+
attempts: story.attempts + 1, // Increment attempts
|
|
1346
|
+
priorErrors: ["ASSET_CHECK_FAILED: Missing file src/finder.ts"],
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
expect(updatedStory.attempts).toBe(5);
|
|
1350
|
+
|
|
1351
|
+
// Now attempts >= tier budget, should escalate on next iteration
|
|
1352
|
+
const tierCfg = defaultTiers.find((t) => t.tier === "fast");
|
|
1353
|
+
expect(updatedStory.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
|
|
1354
|
+
|
|
1355
|
+
const nextTier = escalateTier("fast", defaultTiers);
|
|
1356
|
+
expect(nextTier).toBe("balanced");
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
test("story below tier budget should not escalate", () => {
|
|
1360
|
+
const story: UserStory = {
|
|
1361
|
+
id: "US-006",
|
|
1362
|
+
title: "Story with attempts below budget",
|
|
1363
|
+
description: "Test",
|
|
1364
|
+
acceptanceCriteria: ["AC1"],
|
|
1365
|
+
tags: [],
|
|
1366
|
+
dependencies: [],
|
|
1367
|
+
status: "pending",
|
|
1368
|
+
passes: false,
|
|
1369
|
+
escalations: [],
|
|
1370
|
+
attempts: 2, // Below fast tier budget (5 attempts)
|
|
1371
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
const currentTier = story.routing!.modelTier;
|
|
1375
|
+
const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
|
|
1376
|
+
|
|
1377
|
+
expect(tierCfg).toBeDefined();
|
|
1378
|
+
expect(story.attempts).toBeLessThan(tierCfg!.attempts);
|
|
1379
|
+
|
|
1380
|
+
// Should NOT escalate (continue at same tier)
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1385
|
+
// T6: resolveMaxAttemptsOutcome — failure category → pause vs fail
|
|
1386
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1387
|
+
|
|
1388
|
+
describe("resolveMaxAttemptsOutcome", () => {
|
|
1389
|
+
describe("categories that require human review → pause", () => {
|
|
1390
|
+
test("isolation-violation → pause", () => {
|
|
1391
|
+
const result = resolveMaxAttemptsOutcome("isolation-violation");
|
|
1392
|
+
expect(result).toBe("pause");
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
test("verifier-rejected → pause", () => {
|
|
1396
|
+
const result = resolveMaxAttemptsOutcome("verifier-rejected");
|
|
1397
|
+
expect(result).toBe("pause");
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("greenfield-no-tests → pause", () => {
|
|
1401
|
+
const result = resolveMaxAttemptsOutcome("greenfield-no-tests");
|
|
1402
|
+
expect(result).toBe("pause");
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
describe("categories that can be failed automatically → fail", () => {
|
|
1407
|
+
test("session-failure → fail", () => {
|
|
1408
|
+
const result = resolveMaxAttemptsOutcome("session-failure");
|
|
1409
|
+
expect(result).toBe("fail");
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
test("tests-failing → fail", () => {
|
|
1413
|
+
const result = resolveMaxAttemptsOutcome("tests-failing");
|
|
1414
|
+
expect(result).toBe("fail");
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
test("undefined (no category) → fail", () => {
|
|
1418
|
+
const result = resolveMaxAttemptsOutcome(undefined);
|
|
1419
|
+
expect(result).toBe("fail");
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
describe("exhaustive coverage of all FailureCategory values", () => {
|
|
1424
|
+
const pauseCategories: FailureCategory[] = ["isolation-violation", "verifier-rejected", "greenfield-no-tests"];
|
|
1425
|
+
const failCategories: FailureCategory[] = ["session-failure", "tests-failing"];
|
|
1426
|
+
|
|
1427
|
+
for (const cat of pauseCategories) {
|
|
1428
|
+
test(`${cat} always returns pause`, () => {
|
|
1429
|
+
expect(resolveMaxAttemptsOutcome(cat)).toBe("pause");
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
for (const cat of failCategories) {
|
|
1434
|
+
test(`${cat} always returns fail`, () => {
|
|
1435
|
+
expect(resolveMaxAttemptsOutcome(cat)).toBe("fail");
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1442
|
+
// T6: retryAsLite routing update logic
|
|
1443
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1444
|
+
|
|
1445
|
+
describe("retryAsLite → testStrategy downgrade", () => {
|
|
1446
|
+
/**
|
|
1447
|
+
* Simulates the routing update logic from the escalate case in runner.ts.
|
|
1448
|
+
* This mirrors the exact transform applied to story.routing when escalating.
|
|
1449
|
+
*/
|
|
1450
|
+
function applyEscalationRouting(
|
|
1451
|
+
routing: UserStory["routing"],
|
|
1452
|
+
nextTier: "fast" | "balanced" | "powerful",
|
|
1453
|
+
retryAsLite: boolean,
|
|
1454
|
+
): UserStory["routing"] {
|
|
1455
|
+
if (!routing) return undefined;
|
|
1456
|
+
return {
|
|
1457
|
+
...routing,
|
|
1458
|
+
modelTier: nextTier,
|
|
1459
|
+
...(retryAsLite ? { testStrategy: "three-session-tdd-lite" as const } : {}),
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
test("retryAsLite=true downgrades testStrategy to three-session-tdd-lite", () => {
|
|
1464
|
+
const routing: UserStory["routing"] = {
|
|
1465
|
+
complexity: "complex",
|
|
1466
|
+
modelTier: "fast",
|
|
1467
|
+
testStrategy: "three-session-tdd",
|
|
1468
|
+
reasoning: "complex",
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
const updated = applyEscalationRouting(routing, "balanced", true);
|
|
1472
|
+
|
|
1473
|
+
expect(updated?.testStrategy).toBe("three-session-tdd-lite");
|
|
1474
|
+
expect(updated?.modelTier).toBe("balanced");
|
|
1475
|
+
expect(updated?.complexity).toBe("complex");
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
test("retryAsLite=false leaves testStrategy unchanged", () => {
|
|
1479
|
+
const routing: UserStory["routing"] = {
|
|
1480
|
+
complexity: "complex",
|
|
1481
|
+
modelTier: "fast",
|
|
1482
|
+
testStrategy: "three-session-tdd",
|
|
1483
|
+
reasoning: "complex",
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const updated = applyEscalationRouting(routing, "balanced", false);
|
|
1487
|
+
|
|
1488
|
+
expect(updated?.testStrategy).toBe("three-session-tdd");
|
|
1489
|
+
expect(updated?.modelTier).toBe("balanced");
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
test("strategy downgrade happens alongside tier escalation (both applied)", () => {
|
|
1493
|
+
const routing: UserStory["routing"] = {
|
|
1494
|
+
complexity: "complex",
|
|
1495
|
+
modelTier: "fast",
|
|
1496
|
+
testStrategy: "three-session-tdd",
|
|
1497
|
+
reasoning: "complex",
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
const updated = applyEscalationRouting(routing, "powerful", true);
|
|
1501
|
+
|
|
1502
|
+
// Both tier escalation AND strategy downgrade apply simultaneously
|
|
1503
|
+
expect(updated?.modelTier).toBe("powerful");
|
|
1504
|
+
expect(updated?.testStrategy).toBe("three-session-tdd-lite");
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
test("already-lite strategy remains lite after retryAsLite=true", () => {
|
|
1508
|
+
const routing: UserStory["routing"] = {
|
|
1509
|
+
complexity: "complex",
|
|
1510
|
+
modelTier: "fast",
|
|
1511
|
+
testStrategy: "three-session-tdd-lite",
|
|
1512
|
+
reasoning: "complex",
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
const updated = applyEscalationRouting(routing, "balanced", true);
|
|
1516
|
+
|
|
1517
|
+
expect(updated?.testStrategy).toBe("three-session-tdd-lite");
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test("test-after strategy is not changed by retryAsLite (should not happen, but safe)", () => {
|
|
1521
|
+
const routing: UserStory["routing"] = {
|
|
1522
|
+
complexity: "simple",
|
|
1523
|
+
modelTier: "fast",
|
|
1524
|
+
testStrategy: "test-after",
|
|
1525
|
+
reasoning: "simple",
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
// retryAsLite would only be set for TDD stories, but test correctness:
|
|
1529
|
+
const updated = applyEscalationRouting(routing, "balanced", true);
|
|
1530
|
+
|
|
1531
|
+
// retryAsLite overrides to lite, but this would be a bug in routing
|
|
1532
|
+
// (retryAsLite should only be set when testStrategy is three-session-tdd)
|
|
1533
|
+
expect(updated?.modelTier).toBe("balanced");
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
test("undefined routing returns undefined", () => {
|
|
1537
|
+
const updated = applyEscalationRouting(undefined, "balanced", true);
|
|
1538
|
+
expect(updated).toBeUndefined();
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1543
|
+
// T6: TDD Escalation Attempts Counting
|
|
1544
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1545
|
+
|
|
1546
|
+
describe("TDD escalation attempts counting", () => {
|
|
1547
|
+
const defaultTiers = [
|
|
1548
|
+
{ tier: "fast", attempts: 5 },
|
|
1549
|
+
{ tier: "balanced", attempts: 3 },
|
|
1550
|
+
{ tier: "powerful", attempts: 2 },
|
|
1551
|
+
];
|
|
1552
|
+
|
|
1553
|
+
test("attempts increment on each TDD escalation", () => {
|
|
1554
|
+
// Simulate a TDD story escalating: fast(attempt 1) → balanced(attempt 2) → ...
|
|
1555
|
+
let story: UserStory = {
|
|
1556
|
+
id: "US-001",
|
|
1557
|
+
title: "TDD Story",
|
|
1558
|
+
description: "Complex TDD story",
|
|
1559
|
+
acceptanceCriteria: ["All tests pass"],
|
|
1560
|
+
tags: [],
|
|
1561
|
+
dependencies: [],
|
|
1562
|
+
status: "pending",
|
|
1563
|
+
passes: false,
|
|
1564
|
+
escalations: [],
|
|
1565
|
+
attempts: 0,
|
|
1566
|
+
routing: { complexity: "complex", modelTier: "fast", testStrategy: "three-session-tdd", reasoning: "complex" },
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
// Simulate escalation (what runner does)
|
|
1570
|
+
story = {
|
|
1571
|
+
...story,
|
|
1572
|
+
attempts: story.attempts + 1,
|
|
1573
|
+
routing: story.routing ? { ...story.routing, modelTier: "balanced" } : undefined,
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
expect(story.attempts).toBe(1);
|
|
1577
|
+
expect(story.routing?.modelTier).toBe("balanced");
|
|
1578
|
+
|
|
1579
|
+
// Second escalation
|
|
1580
|
+
story = {
|
|
1581
|
+
...story,
|
|
1582
|
+
attempts: story.attempts + 1,
|
|
1583
|
+
routing: story.routing ? { ...story.routing, modelTier: "powerful" } : undefined,
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
expect(story.attempts).toBe(2);
|
|
1587
|
+
expect(story.routing?.modelTier).toBe("powerful");
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
test("TDD story with retryAsLite gets lite strategy on first isolation-violation escalation", () => {
|
|
1591
|
+
let story: UserStory = {
|
|
1592
|
+
id: "US-001",
|
|
1593
|
+
title: "TDD Story",
|
|
1594
|
+
description: "Complex TDD story",
|
|
1595
|
+
acceptanceCriteria: ["All tests pass"],
|
|
1596
|
+
tags: [],
|
|
1597
|
+
dependencies: [],
|
|
1598
|
+
status: "pending",
|
|
1599
|
+
passes: false,
|
|
1600
|
+
escalations: [],
|
|
1601
|
+
attempts: 0,
|
|
1602
|
+
routing: { complexity: "complex", modelTier: "fast", testStrategy: "three-session-tdd", reasoning: "complex" },
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
// First escalation: isolation-violation → retryAsLite=true
|
|
1606
|
+
const retryAsLite = true;
|
|
1607
|
+
const nextTier = "balanced" as const;
|
|
1608
|
+
|
|
1609
|
+
story = {
|
|
1610
|
+
...story,
|
|
1611
|
+
attempts: story.attempts + 1,
|
|
1612
|
+
routing: story.routing
|
|
1613
|
+
? {
|
|
1614
|
+
...story.routing,
|
|
1615
|
+
modelTier: nextTier,
|
|
1616
|
+
...(retryAsLite ? { testStrategy: "three-session-tdd-lite" as const } : {}),
|
|
1617
|
+
}
|
|
1618
|
+
: undefined,
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
expect(story.attempts).toBe(1);
|
|
1622
|
+
expect(story.routing?.modelTier).toBe("balanced");
|
|
1623
|
+
expect(story.routing?.testStrategy).toBe("three-session-tdd-lite");
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
test("second escalation after retryAsLite does NOT change strategy again", () => {
|
|
1627
|
+
// Story is now in lite mode after first escalation
|
|
1628
|
+
let story: UserStory = {
|
|
1629
|
+
id: "US-001",
|
|
1630
|
+
title: "TDD Story",
|
|
1631
|
+
description: "Complex TDD story",
|
|
1632
|
+
acceptanceCriteria: ["All tests pass"],
|
|
1633
|
+
tags: [],
|
|
1634
|
+
dependencies: [],
|
|
1635
|
+
status: "pending",
|
|
1636
|
+
passes: false,
|
|
1637
|
+
escalations: [],
|
|
1638
|
+
attempts: 1,
|
|
1639
|
+
routing: {
|
|
1640
|
+
complexity: "complex",
|
|
1641
|
+
modelTier: "balanced",
|
|
1642
|
+
testStrategy: "three-session-tdd-lite", // Already downgraded
|
|
1643
|
+
reasoning: "complex",
|
|
1644
|
+
},
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
// Second escalation: lite mode failure → retryAsLite is NOT set (only fires once)
|
|
1648
|
+
const retryAsLite = false; // Not set on subsequent escalations
|
|
1649
|
+
const nextTier = "powerful" as const;
|
|
1650
|
+
|
|
1651
|
+
story = {
|
|
1652
|
+
...story,
|
|
1653
|
+
attempts: story.attempts + 1,
|
|
1654
|
+
routing: story.routing
|
|
1655
|
+
? {
|
|
1656
|
+
...story.routing,
|
|
1657
|
+
modelTier: nextTier,
|
|
1658
|
+
...(retryAsLite ? { testStrategy: "three-session-tdd-lite" as const } : {}),
|
|
1659
|
+
}
|
|
1660
|
+
: undefined,
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
expect(story.attempts).toBe(2);
|
|
1664
|
+
expect(story.routing?.modelTier).toBe("powerful");
|
|
1665
|
+
// Strategy remains lite (not reset) — retryAsLite only fires once
|
|
1666
|
+
expect(story.routing?.testStrategy).toBe("three-session-tdd-lite");
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
test("max attempts check works correctly for TDD stories using total across tiers", () => {
|
|
1670
|
+
const { calculateMaxIterations } = require("../../src/execution/escalation");
|
|
1671
|
+
const maxAttempts = calculateMaxIterations(defaultTiers);
|
|
1672
|
+
|
|
1673
|
+
// A TDD story at attempt 9 (one below max) should still be escalatable
|
|
1674
|
+
expect(9 < maxAttempts).toBe(true);
|
|
1675
|
+
|
|
1676
|
+
// A TDD story at attempt 10 (= max) should NOT be escalatable
|
|
1677
|
+
expect(10 < maxAttempts).toBe(false);
|
|
1678
|
+
});
|
|
1679
|
+
});
|