@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,231 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
COST_RATES,
|
|
4
|
+
estimateCost,
|
|
5
|
+
estimateCostByDuration,
|
|
6
|
+
estimateCostFromOutput,
|
|
7
|
+
formatCostWithConfidence,
|
|
8
|
+
parseTokenUsage,
|
|
9
|
+
} from "../../src/agents/cost";
|
|
10
|
+
|
|
11
|
+
describe("parseTokenUsage", () => {
|
|
12
|
+
test("parses Claude Code token output with estimated confidence", () => {
|
|
13
|
+
const output = `
|
|
14
|
+
Agent completed successfully.
|
|
15
|
+
Input tokens: 12345
|
|
16
|
+
Output tokens: 6789
|
|
17
|
+
Total tokens: 19134
|
|
18
|
+
`;
|
|
19
|
+
const usage = parseTokenUsage(output);
|
|
20
|
+
expect(usage).not.toBeNull();
|
|
21
|
+
expect(usage?.inputTokens).toBe(12345);
|
|
22
|
+
expect(usage?.outputTokens).toBe(6789);
|
|
23
|
+
expect(usage?.confidence).toBe("estimated");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("handles case-insensitive matches with estimated confidence", () => {
|
|
27
|
+
const output = "INPUT TOKENS: 1000\nOUTPUT TOKENS: 2000";
|
|
28
|
+
const usage = parseTokenUsage(output);
|
|
29
|
+
expect(usage).not.toBeNull();
|
|
30
|
+
expect(usage?.inputTokens).toBe(1000);
|
|
31
|
+
expect(usage?.outputTokens).toBe(2000);
|
|
32
|
+
expect(usage?.confidence).toBe("estimated");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns null when tokens not found", () => {
|
|
36
|
+
const output = "Agent completed successfully.";
|
|
37
|
+
expect(parseTokenUsage(output)).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns null when only partial token info", () => {
|
|
41
|
+
const output = "Input tokens: 1000";
|
|
42
|
+
expect(parseTokenUsage(output)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("parses JSON-structured token report with exact confidence (BUG-3)", () => {
|
|
46
|
+
const output = `{"usage": {"input_tokens": 15000, "output_tokens": 8500}}`;
|
|
47
|
+
const usage = parseTokenUsage(output);
|
|
48
|
+
expect(usage).not.toBeNull();
|
|
49
|
+
expect(usage?.inputTokens).toBe(15000);
|
|
50
|
+
expect(usage?.outputTokens).toBe(8500);
|
|
51
|
+
expect(usage?.confidence).toBe("exact");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("parses JSON with surrounding text with exact confidence (BUG-3)", () => {
|
|
55
|
+
const output = `
|
|
56
|
+
Agent completed.
|
|
57
|
+
{"usage": {"input_tokens": 12000, "output_tokens": 4000}}
|
|
58
|
+
Done.
|
|
59
|
+
`;
|
|
60
|
+
const usage = parseTokenUsage(output);
|
|
61
|
+
expect(usage).not.toBeNull();
|
|
62
|
+
expect(usage?.inputTokens).toBe(12000);
|
|
63
|
+
expect(usage?.outputTokens).toBe(4000);
|
|
64
|
+
expect(usage?.confidence).toBe("exact");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("parses underscore format with estimated confidence (BUG-3)", () => {
|
|
68
|
+
const output = "input_tokens: 9000\noutput_tokens: 3000";
|
|
69
|
+
const usage = parseTokenUsage(output);
|
|
70
|
+
expect(usage).not.toBeNull();
|
|
71
|
+
expect(usage?.inputTokens).toBe(9000);
|
|
72
|
+
expect(usage?.outputTokens).toBe(3000);
|
|
73
|
+
expect(usage?.confidence).toBe("estimated");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("rejects unreasonably large token counts (BUG-3 sanity check)", () => {
|
|
77
|
+
const output = "Input tokens: 5000000\nOutput tokens: 2000000";
|
|
78
|
+
// Should reject tokens > 1M as likely false positive
|
|
79
|
+
expect(parseTokenUsage(output)).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("requires at least 2 digits to avoid false positives (BUG-3)", () => {
|
|
83
|
+
const output = "version: 1\ninput: 5\noutput: 8";
|
|
84
|
+
// Should not match single-digit numbers
|
|
85
|
+
expect(parseTokenUsage(output)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("handles mixed format with word boundaries and estimated confidence (BUG-3)", () => {
|
|
89
|
+
const output = `
|
|
90
|
+
Model: claude-sonnet-4-5
|
|
91
|
+
input tokens: 15432
|
|
92
|
+
output tokens: 7891
|
|
93
|
+
Status: success
|
|
94
|
+
`;
|
|
95
|
+
const usage = parseTokenUsage(output);
|
|
96
|
+
expect(usage).not.toBeNull();
|
|
97
|
+
expect(usage?.inputTokens).toBe(15432);
|
|
98
|
+
expect(usage?.outputTokens).toBe(7891);
|
|
99
|
+
expect(usage?.confidence).toBe("estimated");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("estimateCost", () => {
|
|
104
|
+
test("calculates cost for fast tier (Haiku)", () => {
|
|
105
|
+
const cost = estimateCost("fast", 1_000_000, 1_000_000);
|
|
106
|
+
// $0.80 input + $4.00 output = $4.80
|
|
107
|
+
expect(cost).toBeCloseTo(4.8, 2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("calculates cost for balanced tier (Sonnet)", () => {
|
|
111
|
+
const cost = estimateCost("balanced", 1_000_000, 1_000_000);
|
|
112
|
+
// $3.00 input + $15.00 output = $18.00
|
|
113
|
+
expect(cost).toBeCloseTo(18.0, 2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("calculates cost for powerful tier (Opus)", () => {
|
|
117
|
+
const cost = estimateCost("powerful", 1_000_000, 1_000_000);
|
|
118
|
+
// $15.00 input + $75.00 output = $90.00
|
|
119
|
+
expect(cost).toBeCloseTo(90.0, 2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("handles small token counts", () => {
|
|
123
|
+
const cost = estimateCost("fast", 10_000, 5_000);
|
|
124
|
+
// (10k/1M * 0.80) + (5k/1M * 4.00) = 0.008 + 0.020 = 0.028
|
|
125
|
+
expect(cost).toBeCloseTo(0.028, 3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("handles zero tokens", () => {
|
|
129
|
+
const cost = estimateCost("balanced", 0, 0);
|
|
130
|
+
expect(cost).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("estimateCostFromOutput", () => {
|
|
135
|
+
test("estimates cost from parsed output with confidence", () => {
|
|
136
|
+
const output = "Input tokens: 100000\nOutput tokens: 50000";
|
|
137
|
+
const estimate = estimateCostFromOutput("fast", output);
|
|
138
|
+
// (100k/1M * 0.80) + (50k/1M * 4.00) = 0.08 + 0.20 = 0.28
|
|
139
|
+
expect(estimate).not.toBeNull();
|
|
140
|
+
expect(estimate?.cost).toBeCloseTo(0.28, 2);
|
|
141
|
+
expect(estimate?.confidence).toBe("estimated");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("returns exact confidence for JSON output", () => {
|
|
145
|
+
const output = '{"usage": {"input_tokens": 100000, "output_tokens": 50000}}';
|
|
146
|
+
const estimate = estimateCostFromOutput("fast", output);
|
|
147
|
+
expect(estimate).not.toBeNull();
|
|
148
|
+
expect(estimate?.cost).toBeCloseTo(0.28, 2);
|
|
149
|
+
expect(estimate?.confidence).toBe("exact");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("returns null when tokens cannot be parsed", () => {
|
|
153
|
+
const output = "Agent completed successfully.";
|
|
154
|
+
const estimate = estimateCostFromOutput("balanced", output);
|
|
155
|
+
expect(estimate).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("estimateCostByDuration", () => {
|
|
160
|
+
test("estimates cost for 1 minute fast tier with fallback confidence", () => {
|
|
161
|
+
const estimate = estimateCostByDuration("fast", 60000);
|
|
162
|
+
expect(estimate.cost).toBeCloseTo(0.01, 2);
|
|
163
|
+
expect(estimate.confidence).toBe("fallback");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("estimates cost for 2 minutes balanced tier with fallback confidence", () => {
|
|
167
|
+
const estimate = estimateCostByDuration("balanced", 120000);
|
|
168
|
+
expect(estimate.cost).toBeCloseTo(0.1, 2);
|
|
169
|
+
expect(estimate.confidence).toBe("fallback");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("estimates cost for 30 seconds powerful tier with fallback confidence", () => {
|
|
173
|
+
const estimate = estimateCostByDuration("powerful", 30000);
|
|
174
|
+
expect(estimate.cost).toBeCloseTo(0.075, 3);
|
|
175
|
+
expect(estimate.confidence).toBe("fallback");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("handles zero duration with fallback confidence", () => {
|
|
179
|
+
const estimate = estimateCostByDuration("balanced", 0);
|
|
180
|
+
expect(estimate.cost).toBe(0);
|
|
181
|
+
expect(estimate.confidence).toBe("fallback");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("formatCostWithConfidence", () => {
|
|
186
|
+
test("formats exact confidence without prefix", () => {
|
|
187
|
+
const estimate = { cost: 0.12, confidence: "exact" as const };
|
|
188
|
+
expect(formatCostWithConfidence(estimate)).toBe("$0.12");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("formats estimated confidence with tilde prefix", () => {
|
|
192
|
+
const estimate = { cost: 0.15, confidence: "estimated" as const };
|
|
193
|
+
expect(formatCostWithConfidence(estimate)).toBe("~$0.15");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("formats fallback confidence with tilde and label", () => {
|
|
197
|
+
const estimate = { cost: 0.05, confidence: "fallback" as const };
|
|
198
|
+
expect(formatCostWithConfidence(estimate)).toBe("~$0.05 (duration-based)");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("formats very small costs correctly", () => {
|
|
202
|
+
const estimate = { cost: 0.001, confidence: "exact" as const };
|
|
203
|
+
expect(formatCostWithConfidence(estimate)).toBe("$0.00");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("formats large costs correctly", () => {
|
|
207
|
+
const estimate = { cost: 12.345, confidence: "estimated" as const };
|
|
208
|
+
expect(formatCostWithConfidence(estimate)).toBe("~$12.35");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("COST_RATES", () => {
|
|
213
|
+
test("has rates for all model tiers", () => {
|
|
214
|
+
expect(COST_RATES.fast).toBeDefined();
|
|
215
|
+
expect(COST_RATES.balanced).toBeDefined();
|
|
216
|
+
expect(COST_RATES.powerful).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("rates are positive numbers", () => {
|
|
220
|
+
for (const tier of ["fast", "balanced", "powerful"] as const) {
|
|
221
|
+
expect(COST_RATES[tier].inputPer1M).toBeGreaterThan(0);
|
|
222
|
+
expect(COST_RATES[tier].outputPer1M).toBeGreaterThan(0);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("output costs are higher than input costs", () => {
|
|
227
|
+
for (const tier of ["fast", "balanced", "powerful"] as const) {
|
|
228
|
+
expect(COST_RATES[tier].outputPer1M).toBeGreaterThan(COST_RATES[tier].inputPer1M);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for crash recovery module (US-007)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { DEFAULT_CONFIG } from "../../src/config";
|
|
9
|
+
import {
|
|
10
|
+
type CrashRecoveryContext,
|
|
11
|
+
installCrashHandlers,
|
|
12
|
+
resetCrashHandlers,
|
|
13
|
+
startHeartbeat,
|
|
14
|
+
stopHeartbeat,
|
|
15
|
+
writeExitSummary,
|
|
16
|
+
} from "../../src/execution/crash-recovery";
|
|
17
|
+
import { StatusWriter } from "../../src/execution/status-writer";
|
|
18
|
+
|
|
19
|
+
const TEST_DIR = join(import.meta.dir, "..", ".tmp-crash-recovery");
|
|
20
|
+
const TEST_JSONL = join(TEST_DIR, "test.jsonl");
|
|
21
|
+
const TEST_STATUS_FILE = join(TEST_DIR, "status.json");
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
// Create test directory
|
|
25
|
+
if (existsSync(TEST_DIR)) {
|
|
26
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Reset crash handlers before each test
|
|
31
|
+
resetCrashHandlers();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
// Reset crash handlers after each test
|
|
36
|
+
resetCrashHandlers();
|
|
37
|
+
|
|
38
|
+
// Clean up test directory
|
|
39
|
+
if (existsSync(TEST_DIR)) {
|
|
40
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("crash-recovery", () => {
|
|
45
|
+
describe("installCrashHandlers", () => {
|
|
46
|
+
test("should install handlers without throwing", () => {
|
|
47
|
+
const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
|
|
48
|
+
runId: "test-run",
|
|
49
|
+
feature: "test-feature",
|
|
50
|
+
startedAt: new Date().toISOString(),
|
|
51
|
+
dryRun: false,
|
|
52
|
+
startTimeMs: Date.now(),
|
|
53
|
+
pid: process.pid,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const ctx: CrashRecoveryContext = {
|
|
57
|
+
statusWriter,
|
|
58
|
+
getTotalCost: () => 0,
|
|
59
|
+
getIterations: () => 0,
|
|
60
|
+
jsonlFilePath: TEST_JSONL,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
expect(() => installCrashHandlers(ctx)).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("should not throw when called multiple times", () => {
|
|
67
|
+
const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
|
|
68
|
+
runId: "test-run",
|
|
69
|
+
feature: "test-feature",
|
|
70
|
+
startedAt: new Date().toISOString(),
|
|
71
|
+
dryRun: false,
|
|
72
|
+
startTimeMs: Date.now(),
|
|
73
|
+
pid: process.pid,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const ctx: CrashRecoveryContext = {
|
|
77
|
+
statusWriter,
|
|
78
|
+
getTotalCost: () => 0,
|
|
79
|
+
getIterations: () => 0,
|
|
80
|
+
jsonlFilePath: TEST_JSONL,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
installCrashHandlers(ctx);
|
|
84
|
+
expect(() => installCrashHandlers(ctx)).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("heartbeat", () => {
|
|
89
|
+
test("should start and stop heartbeat without throwing", () => {
|
|
90
|
+
const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
|
|
91
|
+
runId: "test-run",
|
|
92
|
+
feature: "test-feature",
|
|
93
|
+
startedAt: new Date().toISOString(),
|
|
94
|
+
dryRun: false,
|
|
95
|
+
startTimeMs: Date.now(),
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const totalCost = 0;
|
|
100
|
+
const iterations = 0;
|
|
101
|
+
|
|
102
|
+
expect(() =>
|
|
103
|
+
startHeartbeat(
|
|
104
|
+
statusWriter,
|
|
105
|
+
() => totalCost,
|
|
106
|
+
() => iterations,
|
|
107
|
+
TEST_JSONL,
|
|
108
|
+
),
|
|
109
|
+
).not.toThrow();
|
|
110
|
+
|
|
111
|
+
expect(() => stopHeartbeat()).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("should write heartbeat entry after interval", async () => {
|
|
115
|
+
const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
|
|
116
|
+
runId: "test-run",
|
|
117
|
+
feature: "test-feature",
|
|
118
|
+
startedAt: new Date().toISOString(),
|
|
119
|
+
dryRun: false,
|
|
120
|
+
startTimeMs: Date.now(),
|
|
121
|
+
pid: process.pid,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
statusWriter.setPrd({
|
|
125
|
+
version: 1,
|
|
126
|
+
feature: "test-feature",
|
|
127
|
+
userStories: [],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const totalCost = 0;
|
|
131
|
+
const iterations = 0;
|
|
132
|
+
|
|
133
|
+
startHeartbeat(
|
|
134
|
+
statusWriter,
|
|
135
|
+
() => totalCost,
|
|
136
|
+
() => iterations,
|
|
137
|
+
TEST_JSONL,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Wait for one heartbeat cycle (60s in production, but we can't wait that long in tests)
|
|
141
|
+
// This test just verifies no crash during startup
|
|
142
|
+
await Bun.sleep(100);
|
|
143
|
+
|
|
144
|
+
stopHeartbeat();
|
|
145
|
+
|
|
146
|
+
// Verify no crash (test passes if we reach here)
|
|
147
|
+
expect(true).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("should stop heartbeat idempotently", () => {
|
|
151
|
+
expect(() => stopHeartbeat()).not.toThrow();
|
|
152
|
+
expect(() => stopHeartbeat()).not.toThrow();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("writeExitSummary", () => {
|
|
157
|
+
test("should write exit summary to JSONL file", async () => {
|
|
158
|
+
await writeExitSummary(TEST_JSONL, 1.23, 5, 3, 60000);
|
|
159
|
+
|
|
160
|
+
const file = Bun.file(TEST_JSONL);
|
|
161
|
+
expect(await file.exists()).toBe(true);
|
|
162
|
+
|
|
163
|
+
const content = await file.text();
|
|
164
|
+
const lines = content.trim().split("\n");
|
|
165
|
+
expect(lines.length).toBe(1);
|
|
166
|
+
|
|
167
|
+
const entry = JSON.parse(lines[0]);
|
|
168
|
+
expect(entry.level).toBe("info");
|
|
169
|
+
expect(entry.stage).toBe("exit-summary");
|
|
170
|
+
expect(entry.message).toBe("Run completed");
|
|
171
|
+
expect(entry.data.totalCost).toBe(1.23);
|
|
172
|
+
expect(entry.data.iterations).toBe(5);
|
|
173
|
+
expect(entry.data.storiesCompleted).toBe(3);
|
|
174
|
+
expect(entry.data.durationMs).toBe(60000);
|
|
175
|
+
expect(entry.data.exitedCleanly).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should not throw when JSONL path is undefined", async () => {
|
|
179
|
+
await expect(writeExitSummary(undefined, 1.23, 5, 3, 60000)).resolves.toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("should include timestamp in exit summary", async () => {
|
|
183
|
+
const beforeTime = new Date().toISOString();
|
|
184
|
+
await writeExitSummary(TEST_JSONL, 1.23, 5, 3, 60000);
|
|
185
|
+
const afterTime = new Date().toISOString();
|
|
186
|
+
|
|
187
|
+
const file = Bun.file(TEST_JSONL);
|
|
188
|
+
const content = await file.text();
|
|
189
|
+
const entry = JSON.parse(content.trim());
|
|
190
|
+
|
|
191
|
+
expect(entry.timestamp).toBeDefined();
|
|
192
|
+
expect(entry.timestamp >= beforeTime).toBe(true);
|
|
193
|
+
expect(entry.timestamp <= afterTime).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("resetCrashHandlers", () => {
|
|
198
|
+
test("should reset handlers and stop heartbeat", () => {
|
|
199
|
+
const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
|
|
200
|
+
runId: "test-run",
|
|
201
|
+
feature: "test-feature",
|
|
202
|
+
startedAt: new Date().toISOString(),
|
|
203
|
+
dryRun: false,
|
|
204
|
+
startTimeMs: Date.now(),
|
|
205
|
+
pid: process.pid,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const ctx: CrashRecoveryContext = {
|
|
209
|
+
statusWriter,
|
|
210
|
+
getTotalCost: () => 0,
|
|
211
|
+
getIterations: () => 0,
|
|
212
|
+
jsonlFilePath: TEST_JSONL,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
installCrashHandlers(ctx);
|
|
216
|
+
startHeartbeat(
|
|
217
|
+
statusWriter,
|
|
218
|
+
() => 0,
|
|
219
|
+
() => 0,
|
|
220
|
+
TEST_JSONL,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(() => resetCrashHandlers()).not.toThrow();
|
|
224
|
+
|
|
225
|
+
// After reset, should be able to install handlers again
|
|
226
|
+
expect(() => installCrashHandlers(ctx)).not.toThrow();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("SIGTERM run.complete event (BUG-017)", () => {
|
|
231
|
+
test("should emit run.complete event when SIGTERM handler is invoked directly", async () => {
|
|
232
|
+
// Mock process.exit to prevent the test process from actually exiting
|
|
233
|
+
const originalExit = process.exit;
|
|
234
|
+
let exitCalled = false;
|
|
235
|
+
let exitCode: number | undefined;
|
|
236
|
+
process.exit = ((code?: number) => {
|
|
237
|
+
exitCalled = true;
|
|
238
|
+
exitCode = code;
|
|
239
|
+
}) as typeof process.exit;
|
|
240
|
+
|
|
241
|
+
const statusWriter = new StatusWriter(TEST_STATUS_FILE, DEFAULT_CONFIG, {
|
|
242
|
+
runId: "run-sigterm-mock",
|
|
243
|
+
feature: "mock-feature",
|
|
244
|
+
startedAt: new Date().toISOString(),
|
|
245
|
+
dryRun: false,
|
|
246
|
+
startTimeMs: Date.now(),
|
|
247
|
+
pid: process.pid,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
statusWriter.setPrd({
|
|
251
|
+
version: 1,
|
|
252
|
+
feature: "mock-feature",
|
|
253
|
+
userStories: [],
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const startTime = Date.now() - 5000;
|
|
257
|
+
const ctx: CrashRecoveryContext = {
|
|
258
|
+
statusWriter,
|
|
259
|
+
getTotalCost: () => 2.5,
|
|
260
|
+
getIterations: () => 4,
|
|
261
|
+
jsonlFilePath: TEST_JSONL,
|
|
262
|
+
runId: "run-sigterm-mock",
|
|
263
|
+
feature: "mock-feature",
|
|
264
|
+
getStartTime: () => startTime,
|
|
265
|
+
getTotalStories: () => 10,
|
|
266
|
+
getStoriesCompleted: () => 3,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const cleanup = installCrashHandlers(ctx);
|
|
270
|
+
|
|
271
|
+
// Capture the SIGTERM listener registered by installCrashHandlers
|
|
272
|
+
const sigtermListeners = process.listeners("SIGTERM");
|
|
273
|
+
const sigtermHandler = sigtermListeners[sigtermListeners.length - 1] as () => Promise<void>;
|
|
274
|
+
|
|
275
|
+
// Invoke the handler directly — no real signal, no subprocess
|
|
276
|
+
await sigtermHandler();
|
|
277
|
+
|
|
278
|
+
// Restore process.exit before assertions
|
|
279
|
+
process.exit = originalExit;
|
|
280
|
+
cleanup();
|
|
281
|
+
|
|
282
|
+
// Assert process.exit was called with the correct SIGTERM exit code
|
|
283
|
+
expect(exitCalled).toBe(true);
|
|
284
|
+
expect(exitCode).toBe(128 + 15); // SIGTERM = signal 15
|
|
285
|
+
|
|
286
|
+
// Assert run.complete event was written to JSONL
|
|
287
|
+
const file = Bun.file(TEST_JSONL);
|
|
288
|
+
expect(await file.exists()).toBe(true);
|
|
289
|
+
const content = await file.text();
|
|
290
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
291
|
+
const entries = lines.map((line: string) => JSON.parse(line));
|
|
292
|
+
const runCompleteEvent = entries.find((e: { stage: string }) => e.stage === "run.complete");
|
|
293
|
+
|
|
294
|
+
expect(runCompleteEvent).toBeDefined();
|
|
295
|
+
expect(runCompleteEvent.level).toBe("info");
|
|
296
|
+
expect(runCompleteEvent.message).toBe("Feature execution terminated");
|
|
297
|
+
expect(runCompleteEvent.data.runId).toBe("run-sigterm-mock");
|
|
298
|
+
expect(runCompleteEvent.data.feature).toBe("mock-feature");
|
|
299
|
+
expect(runCompleteEvent.data.success).toBe(false);
|
|
300
|
+
expect(runCompleteEvent.data.exitReason).toBe("sigterm");
|
|
301
|
+
expect(runCompleteEvent.data.totalCost).toBe(2.5);
|
|
302
|
+
expect(runCompleteEvent.data.iterations).toBe(4);
|
|
303
|
+
expect(runCompleteEvent.data.totalStories).toBe(10);
|
|
304
|
+
expect(runCompleteEvent.data.storiesCompleted).toBe(3);
|
|
305
|
+
expect(runCompleteEvent.data.durationMs).toBeGreaterThanOrEqual(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for src/execution/escalation.ts
|
|
3
|
+
*
|
|
4
|
+
* Covers: escalateTier, getTierConfig, calculateMaxIterations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import type { TierConfig } from "../../src/config";
|
|
9
|
+
import { calculateMaxIterations, escalateTier, getTierConfig } from "../../src/execution/escalation";
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Test fixtures
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const defaultTierOrder: TierConfig[] = [
|
|
16
|
+
{ tier: "fast", attempts: 5 },
|
|
17
|
+
{ tier: "balanced", attempts: 3 },
|
|
18
|
+
{ tier: "powerful", attempts: 2 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const customTierOrder: TierConfig[] = [
|
|
22
|
+
{ tier: "haiku", attempts: 10 },
|
|
23
|
+
{ tier: "sonnet", attempts: 5 },
|
|
24
|
+
{ tier: "opus", attempts: 2 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// escalateTier
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe("escalateTier", () => {
|
|
32
|
+
it("returns next tier in order when not at max", () => {
|
|
33
|
+
expect(escalateTier("fast", defaultTierOrder)).toBe("balanced");
|
|
34
|
+
expect(escalateTier("balanced", defaultTierOrder)).toBe("powerful");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns null when at max tier", () => {
|
|
38
|
+
expect(escalateTier("powerful", defaultTierOrder)).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns null when tier not found in order", () => {
|
|
42
|
+
expect(escalateTier("unknown", defaultTierOrder)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("handles single-tier order", () => {
|
|
46
|
+
const singleTier: TierConfig[] = [{ tier: "only", attempts: 10 }];
|
|
47
|
+
expect(escalateTier("only", singleTier)).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("works with custom tier names", () => {
|
|
51
|
+
expect(escalateTier("haiku", customTierOrder)).toBe("sonnet");
|
|
52
|
+
expect(escalateTier("sonnet", customTierOrder)).toBe("opus");
|
|
53
|
+
expect(escalateTier("opus", customTierOrder)).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns null for empty tier order", () => {
|
|
57
|
+
expect(escalateTier("fast", [])).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// getTierConfig
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe("getTierConfig", () => {
|
|
66
|
+
it("returns tier config when tier exists", () => {
|
|
67
|
+
const config = getTierConfig("balanced", defaultTierOrder);
|
|
68
|
+
expect(config).toEqual({ tier: "balanced", attempts: 3 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns undefined when tier not found", () => {
|
|
72
|
+
expect(getTierConfig("unknown", defaultTierOrder)).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("handles first tier", () => {
|
|
76
|
+
const config = getTierConfig("fast", defaultTierOrder);
|
|
77
|
+
expect(config).toEqual({ tier: "fast", attempts: 5 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles last tier", () => {
|
|
81
|
+
const config = getTierConfig("powerful", defaultTierOrder);
|
|
82
|
+
expect(config).toEqual({ tier: "powerful", attempts: 2 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns undefined for empty tier order", () => {
|
|
86
|
+
expect(getTierConfig("fast", [])).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
// calculateMaxIterations
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe("calculateMaxIterations", () => {
|
|
95
|
+
it("sums all tier attempts", () => {
|
|
96
|
+
// 5 + 3 + 2 = 10
|
|
97
|
+
expect(calculateMaxIterations(defaultTierOrder)).toBe(10);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("handles single tier", () => {
|
|
101
|
+
const singleTier: TierConfig[] = [{ tier: "only", attempts: 7 }];
|
|
102
|
+
expect(calculateMaxIterations(singleTier)).toBe(7);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns 0 for empty tier order", () => {
|
|
106
|
+
expect(calculateMaxIterations([])).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("handles large attempt counts", () => {
|
|
110
|
+
const largeTiers: TierConfig[] = [
|
|
111
|
+
{ tier: "a", attempts: 100 },
|
|
112
|
+
{ tier: "b", attempts: 200 },
|
|
113
|
+
{ tier: "c", attempts: 150 },
|
|
114
|
+
];
|
|
115
|
+
expect(calculateMaxIterations(largeTiers)).toBe(450);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("handles zero attempts", () => {
|
|
119
|
+
const zeroTiers: TierConfig[] = [
|
|
120
|
+
{ tier: "a", attempts: 0 },
|
|
121
|
+
{ tier: "b", attempts: 5 },
|
|
122
|
+
{ tier: "c", attempts: 0 },
|
|
123
|
+
];
|
|
124
|
+
expect(calculateMaxIterations(zeroTiers)).toBe(5);
|
|
125
|
+
});
|
|
126
|
+
});
|