@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,1722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for context builder module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
buildContext,
|
|
11
|
+
createDependencyContext,
|
|
12
|
+
createErrorContext,
|
|
13
|
+
createFileContext,
|
|
14
|
+
createProgressContext,
|
|
15
|
+
createStoryContext,
|
|
16
|
+
estimateTokens,
|
|
17
|
+
formatContextAsMarkdown,
|
|
18
|
+
sortContextElements,
|
|
19
|
+
} from "../../src/context/builder";
|
|
20
|
+
import type { ContextBudget, ContextElement, StoryContext } from "../../src/context/types";
|
|
21
|
+
import type { PRD, UserStory } from "../../src/prd";
|
|
22
|
+
|
|
23
|
+
// Helper to create test PRD
|
|
24
|
+
const createTestPRD = (stories: Partial<UserStory>[]): PRD => ({
|
|
25
|
+
project: "test-project",
|
|
26
|
+
feature: "test-feature",
|
|
27
|
+
branchName: "test-branch",
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
updatedAt: new Date().toISOString(),
|
|
30
|
+
userStories: stories.map((s, i) => ({
|
|
31
|
+
id: s.id || `US-${String(i + 1).padStart(3, "0")}`,
|
|
32
|
+
title: s.title || "Test Story",
|
|
33
|
+
description: s.description || "Test description",
|
|
34
|
+
acceptanceCriteria: s.acceptanceCriteria || ["AC1"],
|
|
35
|
+
dependencies: s.dependencies || [],
|
|
36
|
+
tags: s.tags || [],
|
|
37
|
+
status: s.status || "pending",
|
|
38
|
+
passes: s.passes ?? false,
|
|
39
|
+
escalations: s.escalations || [],
|
|
40
|
+
attempts: s.attempts || 0,
|
|
41
|
+
routing: s.routing,
|
|
42
|
+
priorErrors: s.priorErrors,
|
|
43
|
+
relevantFiles: s.relevantFiles,
|
|
44
|
+
contextFiles: s.contextFiles,
|
|
45
|
+
expectedFiles: s.expectedFiles,
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("Context Builder", () => {
|
|
50
|
+
describe("estimateTokens", () => {
|
|
51
|
+
test("should estimate tokens correctly", () => {
|
|
52
|
+
expect(estimateTokens("test")).toBe(2); // 4 chars = 2 tokens (1 token ≈ 3 chars)
|
|
53
|
+
expect(estimateTokens("hello world")).toBe(4); // 11 chars = 4 tokens
|
|
54
|
+
expect(estimateTokens("")).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("defensive checks", () => {
|
|
59
|
+
test("should handle story with null acceptanceCriteria", async () => {
|
|
60
|
+
// Create PRD directly to bypass helper defaults
|
|
61
|
+
const prd: PRD = {
|
|
62
|
+
project: "test-project",
|
|
63
|
+
feature: "test-feature",
|
|
64
|
+
branchName: "test-branch",
|
|
65
|
+
createdAt: new Date().toISOString(),
|
|
66
|
+
updatedAt: new Date().toISOString(),
|
|
67
|
+
userStories: [
|
|
68
|
+
{
|
|
69
|
+
id: "US-001",
|
|
70
|
+
title: "Malformed Story",
|
|
71
|
+
description: "Test",
|
|
72
|
+
acceptanceCriteria: null as any, // Simulate malformed data
|
|
73
|
+
dependencies: [],
|
|
74
|
+
tags: [],
|
|
75
|
+
status: "pending",
|
|
76
|
+
passes: false,
|
|
77
|
+
escalations: [],
|
|
78
|
+
attempts: 0,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const context: StoryContext = {
|
|
84
|
+
prd,
|
|
85
|
+
currentStoryId: "US-001",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const budget: ContextBudget = {
|
|
89
|
+
maxTokens: 10000,
|
|
90
|
+
reservedForInstructions: 1000,
|
|
91
|
+
availableForContext: 9000,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const built = await buildContext(context, budget);
|
|
95
|
+
expect(built.elements.length).toBeGreaterThan(0);
|
|
96
|
+
const storyElement = built.elements.find((e) => e.type === "story");
|
|
97
|
+
expect(storyElement?.content).toContain("(No acceptance criteria defined)");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("should handle story with undefined acceptanceCriteria", async () => {
|
|
101
|
+
// Create PRD directly to bypass helper defaults
|
|
102
|
+
const prd: PRD = {
|
|
103
|
+
project: "test-project",
|
|
104
|
+
feature: "test-feature",
|
|
105
|
+
branchName: "test-branch",
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
updatedAt: new Date().toISOString(),
|
|
108
|
+
userStories: [
|
|
109
|
+
{
|
|
110
|
+
id: "US-001",
|
|
111
|
+
title: "Malformed Story",
|
|
112
|
+
description: "Test",
|
|
113
|
+
acceptanceCriteria: undefined as any, // Simulate malformed data
|
|
114
|
+
dependencies: [],
|
|
115
|
+
tags: [],
|
|
116
|
+
status: "pending",
|
|
117
|
+
passes: false,
|
|
118
|
+
escalations: [],
|
|
119
|
+
attempts: 0,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const context: StoryContext = {
|
|
125
|
+
prd,
|
|
126
|
+
currentStoryId: "US-001",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const budget: ContextBudget = {
|
|
130
|
+
maxTokens: 10000,
|
|
131
|
+
reservedForInstructions: 1000,
|
|
132
|
+
availableForContext: 9000,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const built = await buildContext(context, budget);
|
|
136
|
+
expect(built.elements.length).toBeGreaterThan(0);
|
|
137
|
+
const storyElement = built.elements.find((e) => e.type === "story");
|
|
138
|
+
expect(storyElement?.content).toContain("(No acceptance criteria defined)");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("should log warning for missing dependency story", async () => {
|
|
142
|
+
const prd = createTestPRD([
|
|
143
|
+
{
|
|
144
|
+
id: "US-001",
|
|
145
|
+
title: "Story with Missing Dependency",
|
|
146
|
+
description: "Test",
|
|
147
|
+
acceptanceCriteria: ["AC1"],
|
|
148
|
+
dependencies: ["US-999"], // Non-existent dependency
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const context: StoryContext = {
|
|
153
|
+
prd,
|
|
154
|
+
currentStoryId: "US-001",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const budget: ContextBudget = {
|
|
158
|
+
maxTokens: 10000,
|
|
159
|
+
reservedForInstructions: 1000,
|
|
160
|
+
availableForContext: 9000,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const built = await buildContext(context, budget);
|
|
164
|
+
|
|
165
|
+
// Should not include the missing dependency in the context
|
|
166
|
+
expect(built.elements.find((e) => e.type === "dependency")).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("should handle story with non-array priorErrors", async () => {
|
|
170
|
+
const prd = createTestPRD([
|
|
171
|
+
{
|
|
172
|
+
id: "US-001",
|
|
173
|
+
title: "Story with Malformed Errors",
|
|
174
|
+
description: "Test",
|
|
175
|
+
acceptanceCriteria: ["AC1"],
|
|
176
|
+
priorErrors: "not an array" as any, // Malformed data
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const context: StoryContext = {
|
|
181
|
+
prd,
|
|
182
|
+
currentStoryId: "US-001",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const budget: ContextBudget = {
|
|
186
|
+
maxTokens: 10000,
|
|
187
|
+
reservedForInstructions: 1000,
|
|
188
|
+
availableForContext: 9000,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const built = await buildContext(context, budget);
|
|
192
|
+
expect(built.elements.find((e) => e.type === "error")).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("createStoryContext", () => {
|
|
197
|
+
test("should create story context element", () => {
|
|
198
|
+
const story: UserStory = {
|
|
199
|
+
id: "US-001",
|
|
200
|
+
title: "Test Story",
|
|
201
|
+
description: "Test description",
|
|
202
|
+
acceptanceCriteria: ["AC1", "AC2"],
|
|
203
|
+
dependencies: [],
|
|
204
|
+
tags: ["feature"],
|
|
205
|
+
status: "pending",
|
|
206
|
+
passes: false,
|
|
207
|
+
escalations: [],
|
|
208
|
+
attempts: 0,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const element = createStoryContext(story, 80);
|
|
212
|
+
|
|
213
|
+
expect(element.type).toBe("story");
|
|
214
|
+
expect(element.storyId).toBe("US-001");
|
|
215
|
+
expect(element.priority).toBe(80);
|
|
216
|
+
expect(element.content).toContain("US-001: Test Story");
|
|
217
|
+
expect(element.content).toContain("Test description");
|
|
218
|
+
expect(element.content).toContain("AC1");
|
|
219
|
+
expect(element.content).toContain("AC2");
|
|
220
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("createDependencyContext", () => {
|
|
225
|
+
test("should create dependency context element", () => {
|
|
226
|
+
const story: UserStory = {
|
|
227
|
+
id: "US-002",
|
|
228
|
+
title: "Dependency Story",
|
|
229
|
+
description: "Dependency description",
|
|
230
|
+
acceptanceCriteria: ["AC1"],
|
|
231
|
+
dependencies: [],
|
|
232
|
+
tags: [],
|
|
233
|
+
status: "passed",
|
|
234
|
+
passes: true,
|
|
235
|
+
escalations: [],
|
|
236
|
+
attempts: 0,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const element = createDependencyContext(story, 50);
|
|
240
|
+
|
|
241
|
+
expect(element.type).toBe("dependency");
|
|
242
|
+
expect(element.storyId).toBe("US-002");
|
|
243
|
+
expect(element.priority).toBe(50);
|
|
244
|
+
expect(element.content).toContain("US-002: Dependency Story");
|
|
245
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("createErrorContext", () => {
|
|
250
|
+
test("should create error context element", () => {
|
|
251
|
+
const error = "TypeError: Cannot read property";
|
|
252
|
+
const element = createErrorContext(error, 90);
|
|
253
|
+
|
|
254
|
+
expect(element.type).toBe("error");
|
|
255
|
+
expect(element.content).toBe(error);
|
|
256
|
+
expect(element.priority).toBe(90);
|
|
257
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("createProgressContext", () => {
|
|
262
|
+
test("should create progress context element", () => {
|
|
263
|
+
const progress = "Progress: 5/12 stories complete (4 passed, 1 failed)";
|
|
264
|
+
const element = createProgressContext(progress, 100);
|
|
265
|
+
|
|
266
|
+
expect(element.type).toBe("progress");
|
|
267
|
+
expect(element.content).toBe(progress);
|
|
268
|
+
expect(element.priority).toBe(100);
|
|
269
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("createFileContext", () => {
|
|
274
|
+
test("should create file context element", () => {
|
|
275
|
+
const filePath = "src/utils/helper.ts";
|
|
276
|
+
const content = 'export function helper() { return "test"; }';
|
|
277
|
+
const element = createFileContext(filePath, content, 60);
|
|
278
|
+
|
|
279
|
+
expect(element.type).toBe("file");
|
|
280
|
+
expect(element.filePath).toBe(filePath);
|
|
281
|
+
expect(element.content).toBe(content);
|
|
282
|
+
expect(element.priority).toBe(60);
|
|
283
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("sortContextElements", () => {
|
|
288
|
+
test("should sort by priority descending", () => {
|
|
289
|
+
const elements: ContextElement[] = [
|
|
290
|
+
createErrorContext("error", 10),
|
|
291
|
+
createProgressContext("progress", 100),
|
|
292
|
+
createErrorContext("error2", 50),
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const sorted = sortContextElements(elements);
|
|
296
|
+
|
|
297
|
+
expect(sorted[0].priority).toBe(100);
|
|
298
|
+
expect(sorted[1].priority).toBe(50);
|
|
299
|
+
expect(sorted[2].priority).toBe(10);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("should sort by tokens ascending for same priority", () => {
|
|
303
|
+
const elements: ContextElement[] = [
|
|
304
|
+
createErrorContext("this is a much longer error message with lots of text", 50),
|
|
305
|
+
createErrorContext("short", 50),
|
|
306
|
+
createErrorContext("medium length message", 50),
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const sorted = sortContextElements(elements);
|
|
310
|
+
|
|
311
|
+
expect(sorted[0].tokens).toBeLessThan(sorted[1].tokens);
|
|
312
|
+
expect(sorted[1].tokens).toBeLessThan(sorted[2].tokens);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("should not mutate original array", () => {
|
|
316
|
+
const elements: ContextElement[] = [createErrorContext("a", 10), createErrorContext("b", 20)];
|
|
317
|
+
|
|
318
|
+
const original = [...elements];
|
|
319
|
+
sortContextElements(elements);
|
|
320
|
+
|
|
321
|
+
expect(elements).toEqual(original);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("buildContext", () => {
|
|
326
|
+
test("should extract current story from PRD", async () => {
|
|
327
|
+
const prd = createTestPRD([
|
|
328
|
+
{
|
|
329
|
+
id: "US-001",
|
|
330
|
+
title: "First Story",
|
|
331
|
+
description: "First description",
|
|
332
|
+
acceptanceCriteria: ["AC1"],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: "US-002",
|
|
336
|
+
title: "Second Story",
|
|
337
|
+
description: "Second description",
|
|
338
|
+
acceptanceCriteria: ["AC2"],
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
const storyContext: StoryContext = {
|
|
343
|
+
prd,
|
|
344
|
+
currentStoryId: "US-001",
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const budget: ContextBudget = {
|
|
348
|
+
maxTokens: 10000,
|
|
349
|
+
reservedForInstructions: 1000,
|
|
350
|
+
availableForContext: 9000,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const built = await buildContext(storyContext, budget);
|
|
354
|
+
|
|
355
|
+
// Should have progress + current story
|
|
356
|
+
expect(built.elements.length).toBe(2);
|
|
357
|
+
expect(built.elements.some((e) => e.type === "progress")).toBe(true);
|
|
358
|
+
expect(built.elements.some((e) => e.type === "story" && e.storyId === "US-001")).toBe(true);
|
|
359
|
+
expect(built.totalTokens).toBeLessThanOrEqual(9000);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("should include dependency stories", async () => {
|
|
363
|
+
const prd = createTestPRD([
|
|
364
|
+
{
|
|
365
|
+
id: "US-001",
|
|
366
|
+
title: "Dependency Story",
|
|
367
|
+
description: "Dependency description",
|
|
368
|
+
acceptanceCriteria: ["AC1"],
|
|
369
|
+
status: "passed",
|
|
370
|
+
passes: true,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: "US-002",
|
|
374
|
+
title: "Current Story",
|
|
375
|
+
description: "Current description",
|
|
376
|
+
acceptanceCriteria: ["AC2"],
|
|
377
|
+
dependencies: ["US-001"],
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
const storyContext: StoryContext = {
|
|
382
|
+
prd,
|
|
383
|
+
currentStoryId: "US-002",
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const budget: ContextBudget = {
|
|
387
|
+
maxTokens: 10000,
|
|
388
|
+
reservedForInstructions: 1000,
|
|
389
|
+
availableForContext: 9000,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const built = await buildContext(storyContext, budget);
|
|
393
|
+
|
|
394
|
+
// Should have progress + current story + dependency
|
|
395
|
+
expect(built.elements.length).toBe(3);
|
|
396
|
+
expect(built.elements.some((e) => e.type === "progress")).toBe(true);
|
|
397
|
+
expect(built.elements.some((e) => e.type === "story" && e.storyId === "US-002")).toBe(true);
|
|
398
|
+
expect(built.elements.some((e) => e.type === "dependency" && e.storyId === "US-001")).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("should include prior errors", async () => {
|
|
402
|
+
const prd = createTestPRD([
|
|
403
|
+
{
|
|
404
|
+
id: "US-001",
|
|
405
|
+
title: "Failed Story",
|
|
406
|
+
description: "Story with errors",
|
|
407
|
+
acceptanceCriteria: ["AC1"],
|
|
408
|
+
priorErrors: ["Error 1", "Error 2"],
|
|
409
|
+
},
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
const storyContext: StoryContext = {
|
|
413
|
+
prd,
|
|
414
|
+
currentStoryId: "US-001",
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const budget: ContextBudget = {
|
|
418
|
+
maxTokens: 10000,
|
|
419
|
+
reservedForInstructions: 1000,
|
|
420
|
+
availableForContext: 9000,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const built = await buildContext(storyContext, budget);
|
|
424
|
+
|
|
425
|
+
const errorElements = built.elements.filter((e) => e.type === "error");
|
|
426
|
+
expect(errorElements.length).toBe(2);
|
|
427
|
+
expect(built.summary).toContain("2 errors");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("should generate progress summary", async () => {
|
|
431
|
+
const prd = createTestPRD([
|
|
432
|
+
{ id: "US-001", status: "passed", passes: true },
|
|
433
|
+
{ id: "US-002", status: "passed", passes: true },
|
|
434
|
+
{ id: "US-003", status: "failed", passes: false },
|
|
435
|
+
{ id: "US-004", status: "pending", passes: false },
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
const storyContext: StoryContext = {
|
|
439
|
+
prd,
|
|
440
|
+
currentStoryId: "US-004",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const budget: ContextBudget = {
|
|
444
|
+
maxTokens: 10000,
|
|
445
|
+
reservedForInstructions: 1000,
|
|
446
|
+
availableForContext: 9000,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const built = await buildContext(storyContext, budget);
|
|
450
|
+
|
|
451
|
+
const progressElement = built.elements.find((e) => e.type === "progress");
|
|
452
|
+
expect(progressElement).toBeDefined();
|
|
453
|
+
expect(progressElement!.content).toContain("3/4 stories complete");
|
|
454
|
+
expect(progressElement!.content).toContain("2 passed");
|
|
455
|
+
expect(progressElement!.content).toContain("1 failed");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("should truncate when exceeding budget", async () => {
|
|
459
|
+
const prd = createTestPRD([
|
|
460
|
+
{
|
|
461
|
+
id: "US-001",
|
|
462
|
+
title: "Story with many dependencies",
|
|
463
|
+
description: "x".repeat(1000),
|
|
464
|
+
acceptanceCriteria: ["AC1"],
|
|
465
|
+
dependencies: ["US-002", "US-003", "US-004", "US-005"],
|
|
466
|
+
},
|
|
467
|
+
{ id: "US-002", description: "x".repeat(1000), acceptanceCriteria: ["AC2"] },
|
|
468
|
+
{ id: "US-003", description: "x".repeat(1000), acceptanceCriteria: ["AC3"] },
|
|
469
|
+
{ id: "US-004", description: "x".repeat(1000), acceptanceCriteria: ["AC4"] },
|
|
470
|
+
{ id: "US-005", description: "x".repeat(1000), acceptanceCriteria: ["AC5"] },
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
const storyContext: StoryContext = {
|
|
474
|
+
prd,
|
|
475
|
+
currentStoryId: "US-001",
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const budget: ContextBudget = {
|
|
479
|
+
maxTokens: 1000,
|
|
480
|
+
reservedForInstructions: 500,
|
|
481
|
+
availableForContext: 500, // Small budget
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const built = await buildContext(storyContext, budget);
|
|
485
|
+
|
|
486
|
+
expect(built.truncated).toBe(true);
|
|
487
|
+
expect(built.totalTokens).toBeLessThanOrEqual(500);
|
|
488
|
+
expect(built.summary).toContain("[TRUNCATED]");
|
|
489
|
+
// Progress should always be included (highest priority)
|
|
490
|
+
expect(built.elements.some((e) => e.type === "progress")).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("should throw error for non-existent story", async () => {
|
|
494
|
+
const prd = createTestPRD([{ id: "US-001", title: "Story" }]);
|
|
495
|
+
|
|
496
|
+
const storyContext: StoryContext = {
|
|
497
|
+
prd,
|
|
498
|
+
currentStoryId: "US-999", // Non-existent
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const budget: ContextBudget = {
|
|
502
|
+
maxTokens: 10000,
|
|
503
|
+
reservedForInstructions: 1000,
|
|
504
|
+
availableForContext: 9000,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
await expect(buildContext(storyContext, budget)).rejects.toThrow("Story US-999 not found in PRD");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("should load files from contextFiles when present", async () => {
|
|
511
|
+
// Create temp directory and files
|
|
512
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
513
|
+
const testFile1 = path.join(tempDir, "helper.ts");
|
|
514
|
+
const testFile2 = path.join(tempDir, "utils.ts");
|
|
515
|
+
|
|
516
|
+
await fs.writeFile(testFile1, 'export function helper() { return "test"; }');
|
|
517
|
+
await fs.writeFile(testFile2, 'export function utils() { return "util"; }');
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const prd = createTestPRD([
|
|
521
|
+
{
|
|
522
|
+
id: "US-001",
|
|
523
|
+
title: "Story with Files",
|
|
524
|
+
description: "Test",
|
|
525
|
+
acceptanceCriteria: ["AC1"],
|
|
526
|
+
contextFiles: ["helper.ts", "utils.ts"],
|
|
527
|
+
},
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
const storyContext: StoryContext = {
|
|
531
|
+
prd,
|
|
532
|
+
currentStoryId: "US-001",
|
|
533
|
+
workdir: tempDir,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const budget: ContextBudget = {
|
|
537
|
+
maxTokens: 10000,
|
|
538
|
+
reservedForInstructions: 1000,
|
|
539
|
+
availableForContext: 9000,
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const built = await buildContext(storyContext, budget);
|
|
543
|
+
|
|
544
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
545
|
+
expect(fileElements.length).toBe(2);
|
|
546
|
+
expect(fileElements[0].filePath).toBe("helper.ts");
|
|
547
|
+
expect(fileElements[1].filePath).toBe("utils.ts");
|
|
548
|
+
expect(fileElements[0].content).toContain("helper()");
|
|
549
|
+
expect(fileElements[1].content).toContain("utils()");
|
|
550
|
+
expect(built.summary).toContain("2 files");
|
|
551
|
+
} finally {
|
|
552
|
+
// Cleanup
|
|
553
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("should fall back to relevantFiles for file loading when contextFiles not present", async () => {
|
|
558
|
+
// Create temp directory and files
|
|
559
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
560
|
+
const testFile = path.join(tempDir, "legacy.ts");
|
|
561
|
+
|
|
562
|
+
await fs.writeFile(testFile, 'export function legacy() { return "old"; }');
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const prd = createTestPRD([
|
|
566
|
+
{
|
|
567
|
+
id: "US-001",
|
|
568
|
+
title: "Legacy Story with relevantFiles",
|
|
569
|
+
description: "Test backward compatibility",
|
|
570
|
+
acceptanceCriteria: ["AC1"],
|
|
571
|
+
relevantFiles: ["legacy.ts"],
|
|
572
|
+
},
|
|
573
|
+
]);
|
|
574
|
+
|
|
575
|
+
const storyContext: StoryContext = {
|
|
576
|
+
prd,
|
|
577
|
+
currentStoryId: "US-001",
|
|
578
|
+
workdir: tempDir,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const budget: ContextBudget = {
|
|
582
|
+
maxTokens: 10000,
|
|
583
|
+
reservedForInstructions: 1000,
|
|
584
|
+
availableForContext: 9000,
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const built = await buildContext(storyContext, budget);
|
|
588
|
+
|
|
589
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
590
|
+
expect(fileElements.length).toBe(1);
|
|
591
|
+
expect(fileElements[0].filePath).toBe("legacy.ts");
|
|
592
|
+
expect(fileElements[0].content).toContain("legacy()");
|
|
593
|
+
} finally {
|
|
594
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("should prefer contextFiles over relevantFiles for file loading", async () => {
|
|
599
|
+
// Create temp directory and files
|
|
600
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
601
|
+
const newFile = path.join(tempDir, "new.ts");
|
|
602
|
+
const oldFile = path.join(tempDir, "old.ts");
|
|
603
|
+
|
|
604
|
+
await fs.writeFile(newFile, "export function newFunc() {}");
|
|
605
|
+
await fs.writeFile(oldFile, "export function oldFunc() {}");
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const prd = createTestPRD([
|
|
609
|
+
{
|
|
610
|
+
id: "US-001",
|
|
611
|
+
title: "Story with both contextFiles and relevantFiles",
|
|
612
|
+
description: "Test precedence",
|
|
613
|
+
acceptanceCriteria: ["AC1"],
|
|
614
|
+
contextFiles: ["new.ts"],
|
|
615
|
+
relevantFiles: ["old.ts"],
|
|
616
|
+
},
|
|
617
|
+
]);
|
|
618
|
+
|
|
619
|
+
const storyContext: StoryContext = {
|
|
620
|
+
prd,
|
|
621
|
+
currentStoryId: "US-001",
|
|
622
|
+
workdir: tempDir,
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const budget: ContextBudget = {
|
|
626
|
+
maxTokens: 10000,
|
|
627
|
+
reservedForInstructions: 1000,
|
|
628
|
+
availableForContext: 9000,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const built = await buildContext(storyContext, budget);
|
|
632
|
+
|
|
633
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
634
|
+
expect(fileElements.length).toBe(1);
|
|
635
|
+
expect(fileElements[0].filePath).toBe("new.ts");
|
|
636
|
+
expect(fileElements[0].content).toContain("newFunc()");
|
|
637
|
+
// Should NOT load old.ts
|
|
638
|
+
expect(fileElements.find((e) => e.filePath === "old.ts")).toBeUndefined();
|
|
639
|
+
} finally {
|
|
640
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("should respect max 5 files limit", async () => {
|
|
645
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
// Create 10 test files
|
|
649
|
+
const files: string[] = [];
|
|
650
|
+
for (let i = 0; i < 10; i++) {
|
|
651
|
+
const filename = `file${i}.ts`;
|
|
652
|
+
files.push(filename);
|
|
653
|
+
await fs.writeFile(path.join(tempDir, filename), `export const file${i} = ${i};`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const prd = createTestPRD([
|
|
657
|
+
{
|
|
658
|
+
id: "US-001",
|
|
659
|
+
title: "Story with Many Files",
|
|
660
|
+
description: "Test",
|
|
661
|
+
acceptanceCriteria: ["AC1"],
|
|
662
|
+
contextFiles: files,
|
|
663
|
+
},
|
|
664
|
+
]);
|
|
665
|
+
|
|
666
|
+
const storyContext: StoryContext = {
|
|
667
|
+
prd,
|
|
668
|
+
currentStoryId: "US-001",
|
|
669
|
+
workdir: tempDir,
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const budget: ContextBudget = {
|
|
673
|
+
maxTokens: 10000,
|
|
674
|
+
reservedForInstructions: 1000,
|
|
675
|
+
availableForContext: 9000,
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const built = await buildContext(storyContext, budget);
|
|
679
|
+
|
|
680
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
681
|
+
expect(fileElements.length).toBe(5); // Max 5 files
|
|
682
|
+
} finally {
|
|
683
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("should skip files larger than 10KB", async () => {
|
|
688
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const smallFile = path.join(tempDir, "small.ts");
|
|
692
|
+
const largeFile = path.join(tempDir, "large.ts");
|
|
693
|
+
|
|
694
|
+
await fs.writeFile(smallFile, 'export const small = "ok";');
|
|
695
|
+
await fs.writeFile(largeFile, "x".repeat(11 * 1024)); // 11KB
|
|
696
|
+
|
|
697
|
+
const prd = createTestPRD([
|
|
698
|
+
{
|
|
699
|
+
id: "US-001",
|
|
700
|
+
title: "Story with Large File",
|
|
701
|
+
description: "Test",
|
|
702
|
+
acceptanceCriteria: ["AC1"],
|
|
703
|
+
contextFiles: ["small.ts", "large.ts"],
|
|
704
|
+
},
|
|
705
|
+
]);
|
|
706
|
+
|
|
707
|
+
const storyContext: StoryContext = {
|
|
708
|
+
prd,
|
|
709
|
+
currentStoryId: "US-001",
|
|
710
|
+
workdir: tempDir,
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const budget: ContextBudget = {
|
|
714
|
+
maxTokens: 20000,
|
|
715
|
+
reservedForInstructions: 1000,
|
|
716
|
+
availableForContext: 19000,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// Capture warnings
|
|
720
|
+
const originalWarn = console.warn;
|
|
721
|
+
const warnings: string[] = [];
|
|
722
|
+
console.warn = (msg: string) => warnings.push(msg);
|
|
723
|
+
|
|
724
|
+
const built = await buildContext(storyContext, budget);
|
|
725
|
+
|
|
726
|
+
console.warn = originalWarn;
|
|
727
|
+
|
|
728
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
729
|
+
expect(fileElements.length).toBe(1); // Only small file loaded
|
|
730
|
+
expect(fileElements[0].filePath).toBe("small.ts");
|
|
731
|
+
// Large file should be skipped (warning logged via structured logger)
|
|
732
|
+
} finally {
|
|
733
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("should warn on missing files", async () => {
|
|
738
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
739
|
+
|
|
740
|
+
try {
|
|
741
|
+
const prd = createTestPRD([
|
|
742
|
+
{
|
|
743
|
+
id: "US-001",
|
|
744
|
+
title: "Story with Missing File",
|
|
745
|
+
description: "Test",
|
|
746
|
+
acceptanceCriteria: ["AC1"],
|
|
747
|
+
contextFiles: ["nonexistent.ts"],
|
|
748
|
+
},
|
|
749
|
+
]);
|
|
750
|
+
|
|
751
|
+
const storyContext: StoryContext = {
|
|
752
|
+
prd,
|
|
753
|
+
currentStoryId: "US-001",
|
|
754
|
+
workdir: tempDir,
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const budget: ContextBudget = {
|
|
758
|
+
maxTokens: 10000,
|
|
759
|
+
reservedForInstructions: 1000,
|
|
760
|
+
availableForContext: 9000,
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const built = await buildContext(storyContext, budget);
|
|
764
|
+
|
|
765
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
766
|
+
expect(fileElements.length).toBe(0);
|
|
767
|
+
// Missing file should be skipped (warning logged via structured logger)
|
|
768
|
+
} finally {
|
|
769
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("should handle empty contextFiles array", async () => {
|
|
774
|
+
const prd = createTestPRD([
|
|
775
|
+
{
|
|
776
|
+
id: "US-001",
|
|
777
|
+
title: "Story with Empty Files",
|
|
778
|
+
description: "Test",
|
|
779
|
+
acceptanceCriteria: ["AC1"],
|
|
780
|
+
contextFiles: [],
|
|
781
|
+
},
|
|
782
|
+
]);
|
|
783
|
+
|
|
784
|
+
const storyContext: StoryContext = {
|
|
785
|
+
prd,
|
|
786
|
+
currentStoryId: "US-001",
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const budget: ContextBudget = {
|
|
790
|
+
maxTokens: 10000,
|
|
791
|
+
reservedForInstructions: 1000,
|
|
792
|
+
availableForContext: 9000,
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const built = await buildContext(storyContext, budget);
|
|
796
|
+
|
|
797
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
798
|
+
expect(fileElements.length).toBe(0);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test("should respect token budget when loading files", async () => {
|
|
802
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
// Create files with substantial content
|
|
806
|
+
await fs.writeFile(path.join(tempDir, "file1.ts"), "x".repeat(5000));
|
|
807
|
+
await fs.writeFile(path.join(tempDir, "file2.ts"), "x".repeat(5000));
|
|
808
|
+
|
|
809
|
+
const prd = createTestPRD([
|
|
810
|
+
{
|
|
811
|
+
id: "US-001",
|
|
812
|
+
title: "Story",
|
|
813
|
+
description: "x".repeat(1000),
|
|
814
|
+
acceptanceCriteria: ["AC1"],
|
|
815
|
+
contextFiles: ["file1.ts", "file2.ts"],
|
|
816
|
+
},
|
|
817
|
+
]);
|
|
818
|
+
|
|
819
|
+
const storyContext: StoryContext = {
|
|
820
|
+
prd,
|
|
821
|
+
currentStoryId: "US-001",
|
|
822
|
+
workdir: tempDir,
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const budget: ContextBudget = {
|
|
826
|
+
maxTokens: 2000,
|
|
827
|
+
reservedForInstructions: 500,
|
|
828
|
+
availableForContext: 1500, // Small budget
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const built = await buildContext(storyContext, budget);
|
|
832
|
+
|
|
833
|
+
expect(built.totalTokens).toBeLessThanOrEqual(1500);
|
|
834
|
+
// Files have lower priority (60) than story (80), so story should be included
|
|
835
|
+
expect(built.elements.some((e) => e.type === "story")).toBe(true);
|
|
836
|
+
} finally {
|
|
837
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
describe("formatContextAsMarkdown", () => {
|
|
843
|
+
test("should format context with all element types", async () => {
|
|
844
|
+
const prd = createTestPRD([
|
|
845
|
+
{
|
|
846
|
+
id: "US-001",
|
|
847
|
+
title: "Dependency",
|
|
848
|
+
description: "Dep description",
|
|
849
|
+
acceptanceCriteria: ["AC1"],
|
|
850
|
+
status: "passed",
|
|
851
|
+
passes: true,
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
id: "US-002",
|
|
855
|
+
title: "Current",
|
|
856
|
+
description: "Current description",
|
|
857
|
+
acceptanceCriteria: ["AC2"],
|
|
858
|
+
dependencies: ["US-001"],
|
|
859
|
+
priorErrors: ["Test error"],
|
|
860
|
+
},
|
|
861
|
+
]);
|
|
862
|
+
|
|
863
|
+
const storyContext: StoryContext = {
|
|
864
|
+
prd,
|
|
865
|
+
currentStoryId: "US-002",
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const budget: ContextBudget = {
|
|
869
|
+
maxTokens: 10000,
|
|
870
|
+
reservedForInstructions: 1000,
|
|
871
|
+
availableForContext: 9000,
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const built = await buildContext(storyContext, budget);
|
|
875
|
+
const markdown = formatContextAsMarkdown(built);
|
|
876
|
+
|
|
877
|
+
expect(markdown).toContain("# Story Context");
|
|
878
|
+
expect(markdown).toContain("## Progress");
|
|
879
|
+
expect(markdown).toContain("## Prior Errors");
|
|
880
|
+
expect(markdown).toContain("## Current Story");
|
|
881
|
+
expect(markdown).toContain("## Dependency Stories");
|
|
882
|
+
expect(markdown).toContain("US-001");
|
|
883
|
+
expect(markdown).toContain("US-002");
|
|
884
|
+
expect(markdown).toContain("Test error");
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("should include summary with token count", async () => {
|
|
888
|
+
const prd = createTestPRD([{ id: "US-001", title: "Story" }]);
|
|
889
|
+
|
|
890
|
+
const storyContext: StoryContext = {
|
|
891
|
+
prd,
|
|
892
|
+
currentStoryId: "US-001",
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const budget: ContextBudget = {
|
|
896
|
+
maxTokens: 10000,
|
|
897
|
+
reservedForInstructions: 1000,
|
|
898
|
+
availableForContext: 9000,
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const built = await buildContext(storyContext, budget);
|
|
902
|
+
const markdown = formatContextAsMarkdown(built);
|
|
903
|
+
|
|
904
|
+
expect(markdown).toContain("Context:");
|
|
905
|
+
expect(markdown).toContain("tokens");
|
|
906
|
+
expect(markdown).toContain(built.totalTokens.toString());
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("should show truncation indicator", async () => {
|
|
910
|
+
const prd = createTestPRD([
|
|
911
|
+
{
|
|
912
|
+
id: "US-001",
|
|
913
|
+
description: "x".repeat(2000),
|
|
914
|
+
dependencies: ["US-002", "US-003"],
|
|
915
|
+
},
|
|
916
|
+
{ id: "US-002", description: "x".repeat(2000) },
|
|
917
|
+
{ id: "US-003", description: "x".repeat(2000) },
|
|
918
|
+
]);
|
|
919
|
+
|
|
920
|
+
const storyContext: StoryContext = {
|
|
921
|
+
prd,
|
|
922
|
+
currentStoryId: "US-001",
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const budget: ContextBudget = {
|
|
926
|
+
maxTokens: 500,
|
|
927
|
+
reservedForInstructions: 250,
|
|
928
|
+
availableForContext: 250,
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const built = await buildContext(storyContext, budget);
|
|
932
|
+
const markdown = formatContextAsMarkdown(built);
|
|
933
|
+
|
|
934
|
+
expect(markdown).toContain("[TRUNCATED]");
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test("should format context with file elements", async () => {
|
|
938
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
await fs.writeFile(path.join(tempDir, "helper.ts"), "export function helper() {}");
|
|
942
|
+
|
|
943
|
+
const prd = createTestPRD([
|
|
944
|
+
{
|
|
945
|
+
id: "US-001",
|
|
946
|
+
title: "Story with File",
|
|
947
|
+
description: "Test",
|
|
948
|
+
acceptanceCriteria: ["AC1"],
|
|
949
|
+
contextFiles: ["helper.ts"],
|
|
950
|
+
},
|
|
951
|
+
]);
|
|
952
|
+
|
|
953
|
+
const storyContext: StoryContext = {
|
|
954
|
+
prd,
|
|
955
|
+
currentStoryId: "US-001",
|
|
956
|
+
workdir: tempDir,
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const budget: ContextBudget = {
|
|
960
|
+
maxTokens: 10000,
|
|
961
|
+
reservedForInstructions: 1000,
|
|
962
|
+
availableForContext: 9000,
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const built = await buildContext(storyContext, budget);
|
|
966
|
+
const markdown = formatContextAsMarkdown(built);
|
|
967
|
+
|
|
968
|
+
expect(markdown).toContain("# Story Context");
|
|
969
|
+
expect(markdown).toContain("## Relevant Source Files");
|
|
970
|
+
expect(markdown).toContain("helper.ts");
|
|
971
|
+
expect(markdown).toContain("helper()");
|
|
972
|
+
} finally {
|
|
973
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("should format ASSET_CHECK_FAILED errors as mandatory instructions", async () => {
|
|
978
|
+
const prd = createTestPRD([
|
|
979
|
+
{
|
|
980
|
+
id: "US-001",
|
|
981
|
+
title: "Story with asset check failure",
|
|
982
|
+
description: "Test description",
|
|
983
|
+
acceptanceCriteria: ["AC1"],
|
|
984
|
+
priorErrors: [
|
|
985
|
+
"ASSET_CHECK_FAILED: Missing files: [src/finder.ts, test/finder.test.ts]\nAction: Create the missing files before tests can run.",
|
|
986
|
+
],
|
|
987
|
+
},
|
|
988
|
+
]);
|
|
989
|
+
|
|
990
|
+
const storyContext: StoryContext = {
|
|
991
|
+
prd,
|
|
992
|
+
currentStoryId: "US-001",
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const budget: ContextBudget = {
|
|
996
|
+
maxTokens: 10000,
|
|
997
|
+
reservedForInstructions: 1000,
|
|
998
|
+
availableForContext: 9000,
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
const built = await buildContext(storyContext, budget);
|
|
1002
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1003
|
+
|
|
1004
|
+
// Verify ASSET_CHECK errors are formatted prominently
|
|
1005
|
+
expect(markdown).toContain("⚠️ MANDATORY: Missing Files from Previous Attempts");
|
|
1006
|
+
expect(markdown).toContain("CRITICAL");
|
|
1007
|
+
expect(markdown).toContain("You MUST create these exact files");
|
|
1008
|
+
expect(markdown).toContain("Do NOT use alternative filenames");
|
|
1009
|
+
expect(markdown).toContain("**Required files:**");
|
|
1010
|
+
expect(markdown).toContain("`src/finder.ts`");
|
|
1011
|
+
expect(markdown).toContain("`test/finder.test.ts`");
|
|
1012
|
+
|
|
1013
|
+
// Verify it's NOT in the generic "Prior Errors" section
|
|
1014
|
+
expect(markdown).not.toContain("## Prior Errors");
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
test("should format mixed ASSET_CHECK and other errors separately", async () => {
|
|
1018
|
+
const prd = createTestPRD([
|
|
1019
|
+
{
|
|
1020
|
+
id: "US-001",
|
|
1021
|
+
title: "Story with multiple error types",
|
|
1022
|
+
description: "Test description",
|
|
1023
|
+
acceptanceCriteria: ["AC1"],
|
|
1024
|
+
priorErrors: [
|
|
1025
|
+
"ASSET_CHECK_FAILED: Missing files: [src/utils.ts]\nAction: Create the missing files before tests can run.",
|
|
1026
|
+
'TypeError: Cannot read property "foo" of undefined',
|
|
1027
|
+
"Test execution failed",
|
|
1028
|
+
],
|
|
1029
|
+
},
|
|
1030
|
+
]);
|
|
1031
|
+
|
|
1032
|
+
const storyContext: StoryContext = {
|
|
1033
|
+
prd,
|
|
1034
|
+
currentStoryId: "US-001",
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const budget: ContextBudget = {
|
|
1038
|
+
maxTokens: 10000,
|
|
1039
|
+
reservedForInstructions: 1000,
|
|
1040
|
+
availableForContext: 9000,
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const built = await buildContext(storyContext, budget);
|
|
1044
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1045
|
+
|
|
1046
|
+
// Verify ASSET_CHECK errors section exists
|
|
1047
|
+
expect(markdown).toContain("⚠️ MANDATORY: Missing Files from Previous Attempts");
|
|
1048
|
+
expect(markdown).toContain("`src/utils.ts`");
|
|
1049
|
+
|
|
1050
|
+
// Verify other errors are in separate section
|
|
1051
|
+
expect(markdown).toContain("## Prior Errors");
|
|
1052
|
+
expect(markdown).toContain('TypeError: Cannot read property "foo" of undefined');
|
|
1053
|
+
expect(markdown).toContain("Test execution failed");
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
test("should handle non-ASSET_CHECK errors normally", async () => {
|
|
1057
|
+
const prd = createTestPRD([
|
|
1058
|
+
{
|
|
1059
|
+
id: "US-001",
|
|
1060
|
+
title: "Story with regular errors",
|
|
1061
|
+
description: "Test description",
|
|
1062
|
+
acceptanceCriteria: ["AC1"],
|
|
1063
|
+
priorErrors: ['TypeError: Cannot read property "foo" of undefined', "Test execution failed"],
|
|
1064
|
+
},
|
|
1065
|
+
]);
|
|
1066
|
+
|
|
1067
|
+
const storyContext: StoryContext = {
|
|
1068
|
+
prd,
|
|
1069
|
+
currentStoryId: "US-001",
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
const budget: ContextBudget = {
|
|
1073
|
+
maxTokens: 10000,
|
|
1074
|
+
reservedForInstructions: 1000,
|
|
1075
|
+
availableForContext: 9000,
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const built = await buildContext(storyContext, budget);
|
|
1079
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1080
|
+
|
|
1081
|
+
// Verify only "Prior Errors" section exists (no MANDATORY section)
|
|
1082
|
+
expect(markdown).toContain("## Prior Errors");
|
|
1083
|
+
expect(markdown).toContain('TypeError: Cannot read property "foo" of undefined');
|
|
1084
|
+
expect(markdown).toContain("Test execution failed");
|
|
1085
|
+
expect(markdown).not.toContain("⚠️ MANDATORY");
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
describe("test coverage scoping", () => {
|
|
1090
|
+
test("should scope test coverage to story contextFiles", async () => {
|
|
1091
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
// Create test directory and files
|
|
1095
|
+
const testDir = path.join(tempDir, "test");
|
|
1096
|
+
await fs.mkdir(testDir);
|
|
1097
|
+
|
|
1098
|
+
// Create multiple test files
|
|
1099
|
+
await fs.writeFile(
|
|
1100
|
+
path.join(testDir, "health.service.test.ts"),
|
|
1101
|
+
'describe("Health Service", () => { test("checks health", () => {}); });',
|
|
1102
|
+
);
|
|
1103
|
+
await fs.writeFile(
|
|
1104
|
+
path.join(testDir, "auth.service.test.ts"),
|
|
1105
|
+
'describe("Auth Service", () => { test("authenticates", () => {}); });',
|
|
1106
|
+
);
|
|
1107
|
+
await fs.writeFile(
|
|
1108
|
+
path.join(testDir, "db.connection.test.ts"),
|
|
1109
|
+
'describe("DB Connection", () => { test("connects", () => {}); });',
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
const prd = createTestPRD([
|
|
1113
|
+
{
|
|
1114
|
+
id: "US-001",
|
|
1115
|
+
title: "Implement health service",
|
|
1116
|
+
description: "Create health check service",
|
|
1117
|
+
acceptanceCriteria: ["Service works"],
|
|
1118
|
+
contextFiles: ["src/health.service.ts"], // Only health service
|
|
1119
|
+
},
|
|
1120
|
+
]);
|
|
1121
|
+
|
|
1122
|
+
const storyContext: StoryContext = {
|
|
1123
|
+
prd,
|
|
1124
|
+
currentStoryId: "US-001",
|
|
1125
|
+
workdir: tempDir,
|
|
1126
|
+
config: {
|
|
1127
|
+
context: {
|
|
1128
|
+
testCoverage: {
|
|
1129
|
+
enabled: true,
|
|
1130
|
+
scopeToStory: true, // Enable scoping
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
} as any,
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
const budget: ContextBudget = {
|
|
1137
|
+
maxTokens: 10000,
|
|
1138
|
+
reservedForInstructions: 1000,
|
|
1139
|
+
availableForContext: 9000,
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
const built = await buildContext(storyContext, budget);
|
|
1143
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1144
|
+
|
|
1145
|
+
// Should include test coverage element
|
|
1146
|
+
expect(built.elements.some((e) => e.type === "test-coverage")).toBe(true);
|
|
1147
|
+
|
|
1148
|
+
// Should only mention health.service.test.ts, not auth or db tests
|
|
1149
|
+
expect(markdown).toContain("health.service.test.ts");
|
|
1150
|
+
expect(markdown).not.toContain("auth.service.test.ts");
|
|
1151
|
+
expect(markdown).not.toContain("db.connection.test.ts");
|
|
1152
|
+
} finally {
|
|
1153
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("should scan all tests when scopeToStory=false", async () => {
|
|
1158
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1159
|
+
|
|
1160
|
+
try {
|
|
1161
|
+
const testDir = path.join(tempDir, "test");
|
|
1162
|
+
await fs.mkdir(testDir);
|
|
1163
|
+
|
|
1164
|
+
await fs.writeFile(
|
|
1165
|
+
path.join(testDir, "health.test.ts"),
|
|
1166
|
+
'describe("Health", () => { test("works", () => {}); });',
|
|
1167
|
+
);
|
|
1168
|
+
await fs.writeFile(path.join(testDir, "auth.test.ts"), 'describe("Auth", () => { test("works", () => {}); });');
|
|
1169
|
+
|
|
1170
|
+
const prd = createTestPRD([
|
|
1171
|
+
{
|
|
1172
|
+
id: "US-001",
|
|
1173
|
+
title: "Story",
|
|
1174
|
+
description: "Test",
|
|
1175
|
+
acceptanceCriteria: ["AC1"],
|
|
1176
|
+
contextFiles: ["src/health.ts"],
|
|
1177
|
+
},
|
|
1178
|
+
]);
|
|
1179
|
+
|
|
1180
|
+
const storyContext: StoryContext = {
|
|
1181
|
+
prd,
|
|
1182
|
+
currentStoryId: "US-001",
|
|
1183
|
+
workdir: tempDir,
|
|
1184
|
+
config: {
|
|
1185
|
+
context: {
|
|
1186
|
+
testCoverage: {
|
|
1187
|
+
enabled: true,
|
|
1188
|
+
scopeToStory: false, // Disabled - should scan all
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
} as any,
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const budget: ContextBudget = {
|
|
1195
|
+
maxTokens: 10000,
|
|
1196
|
+
reservedForInstructions: 1000,
|
|
1197
|
+
availableForContext: 9000,
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
const built = await buildContext(storyContext, budget);
|
|
1201
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1202
|
+
|
|
1203
|
+
// Should include both test files
|
|
1204
|
+
expect(markdown).toContain("health.test.ts");
|
|
1205
|
+
expect(markdown).toContain("auth.test.ts");
|
|
1206
|
+
} finally {
|
|
1207
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test("should fall back to full scan when no contextFiles provided", async () => {
|
|
1212
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1213
|
+
|
|
1214
|
+
try {
|
|
1215
|
+
const testDir = path.join(tempDir, "test");
|
|
1216
|
+
await fs.mkdir(testDir);
|
|
1217
|
+
|
|
1218
|
+
await fs.writeFile(
|
|
1219
|
+
path.join(testDir, "test1.test.ts"),
|
|
1220
|
+
'describe("Test1", () => { test("works", () => {}); });',
|
|
1221
|
+
);
|
|
1222
|
+
await fs.writeFile(
|
|
1223
|
+
path.join(testDir, "test2.test.ts"),
|
|
1224
|
+
'describe("Test2", () => { test("works", () => {}); });',
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
const prd = createTestPRD([
|
|
1228
|
+
{
|
|
1229
|
+
id: "US-001",
|
|
1230
|
+
title: "Story without contextFiles",
|
|
1231
|
+
description: "Test",
|
|
1232
|
+
acceptanceCriteria: ["AC1"],
|
|
1233
|
+
// No contextFiles
|
|
1234
|
+
},
|
|
1235
|
+
]);
|
|
1236
|
+
|
|
1237
|
+
const storyContext: StoryContext = {
|
|
1238
|
+
prd,
|
|
1239
|
+
currentStoryId: "US-001",
|
|
1240
|
+
workdir: tempDir,
|
|
1241
|
+
config: {
|
|
1242
|
+
context: {
|
|
1243
|
+
testCoverage: {
|
|
1244
|
+
enabled: true,
|
|
1245
|
+
scopeToStory: true, // true but no contextFiles
|
|
1246
|
+
},
|
|
1247
|
+
},
|
|
1248
|
+
} as any,
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
const budget: ContextBudget = {
|
|
1252
|
+
maxTokens: 10000,
|
|
1253
|
+
reservedForInstructions: 1000,
|
|
1254
|
+
availableForContext: 9000,
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const built = await buildContext(storyContext, budget);
|
|
1258
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1259
|
+
|
|
1260
|
+
// Should fall back to scanning all files
|
|
1261
|
+
expect(markdown).toContain("test1.test.ts");
|
|
1262
|
+
expect(markdown).toContain("test2.test.ts");
|
|
1263
|
+
} finally {
|
|
1264
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
describe("context isolation", () => {
|
|
1270
|
+
test("should only include current story and declared dependencies — no other stories", async () => {
|
|
1271
|
+
const prd = createTestPRD([
|
|
1272
|
+
{
|
|
1273
|
+
id: "US-001",
|
|
1274
|
+
title: "Define core interfaces",
|
|
1275
|
+
description: "Create base interfaces for the module",
|
|
1276
|
+
acceptanceCriteria: ["Interface exported", "Types documented"],
|
|
1277
|
+
dependencies: [],
|
|
1278
|
+
status: "passed" as any,
|
|
1279
|
+
passes: true,
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
id: "US-002",
|
|
1283
|
+
title: "Implement health service",
|
|
1284
|
+
description: "Service that aggregates indicators",
|
|
1285
|
+
acceptanceCriteria: ["Service injectable", "Aggregates results"],
|
|
1286
|
+
dependencies: [],
|
|
1287
|
+
status: "passed" as any,
|
|
1288
|
+
passes: true,
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
id: "US-003",
|
|
1292
|
+
title: "Add HTTP indicator",
|
|
1293
|
+
description: "HTTP health check indicator",
|
|
1294
|
+
acceptanceCriteria: ["Pings endpoint", "Returns status"],
|
|
1295
|
+
dependencies: ["US-001"],
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
id: "US-004",
|
|
1299
|
+
title: "Add database indicator",
|
|
1300
|
+
description: "Database connectivity check",
|
|
1301
|
+
acceptanceCriteria: ["Checks DB connection", "Timeout support"],
|
|
1302
|
+
dependencies: ["US-001"],
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
id: "US-005",
|
|
1306
|
+
title: "REST endpoint",
|
|
1307
|
+
description: "Expose health check via REST API",
|
|
1308
|
+
acceptanceCriteria: ["GET /health returns JSON", "Includes all indicators"],
|
|
1309
|
+
dependencies: ["US-002", "US-003"],
|
|
1310
|
+
},
|
|
1311
|
+
]);
|
|
1312
|
+
|
|
1313
|
+
// Build context for US-003 which depends only on US-001
|
|
1314
|
+
const storyContext: StoryContext = {
|
|
1315
|
+
prd,
|
|
1316
|
+
currentStoryId: "US-003",
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const budget: ContextBudget = {
|
|
1320
|
+
maxTokens: 50000,
|
|
1321
|
+
reservedForInstructions: 5000,
|
|
1322
|
+
availableForContext: 45000,
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
const built = await buildContext(storyContext, budget);
|
|
1326
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1327
|
+
|
|
1328
|
+
// Current story IS present
|
|
1329
|
+
expect(markdown).toContain("US-003");
|
|
1330
|
+
expect(markdown).toContain("Add HTTP indicator");
|
|
1331
|
+
|
|
1332
|
+
// Declared dependency IS present
|
|
1333
|
+
expect(markdown).toContain("US-001");
|
|
1334
|
+
expect(markdown).toContain("Define core interfaces");
|
|
1335
|
+
|
|
1336
|
+
// Non-dependency stories are NOT present
|
|
1337
|
+
expect(markdown).not.toContain("US-002");
|
|
1338
|
+
expect(markdown).not.toContain("Implement health service");
|
|
1339
|
+
expect(markdown).not.toContain("US-004");
|
|
1340
|
+
expect(markdown).not.toContain("Add database indicator");
|
|
1341
|
+
expect(markdown).not.toContain("US-005");
|
|
1342
|
+
expect(markdown).not.toContain("REST endpoint");
|
|
1343
|
+
|
|
1344
|
+
// Acceptance criteria from other stories are NOT leaked
|
|
1345
|
+
expect(markdown).not.toContain("Aggregates results");
|
|
1346
|
+
expect(markdown).not.toContain("Checks DB connection");
|
|
1347
|
+
expect(markdown).not.toContain("Includes all indicators");
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test("progress summary contains only aggregate counts, not story titles or IDs", async () => {
|
|
1351
|
+
const prd = createTestPRD([
|
|
1352
|
+
{
|
|
1353
|
+
id: "US-001",
|
|
1354
|
+
title: "Secret Story Alpha",
|
|
1355
|
+
description: "Should not appear in progress",
|
|
1356
|
+
acceptanceCriteria: ["AC1"],
|
|
1357
|
+
dependencies: [],
|
|
1358
|
+
status: "passed" as any,
|
|
1359
|
+
passes: true,
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
id: "US-002",
|
|
1363
|
+
title: "Secret Story Beta",
|
|
1364
|
+
description: "Also should not appear",
|
|
1365
|
+
acceptanceCriteria: ["AC1"],
|
|
1366
|
+
dependencies: [],
|
|
1367
|
+
status: "failed" as any,
|
|
1368
|
+
passes: false,
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
id: "US-003",
|
|
1372
|
+
title: "Current Story",
|
|
1373
|
+
description: "The one being built",
|
|
1374
|
+
acceptanceCriteria: ["AC1"],
|
|
1375
|
+
dependencies: [],
|
|
1376
|
+
},
|
|
1377
|
+
]);
|
|
1378
|
+
|
|
1379
|
+
const storyContext: StoryContext = {
|
|
1380
|
+
prd,
|
|
1381
|
+
currentStoryId: "US-003",
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const budget: ContextBudget = {
|
|
1385
|
+
maxTokens: 50000,
|
|
1386
|
+
reservedForInstructions: 5000,
|
|
1387
|
+
availableForContext: 45000,
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
const built = await buildContext(storyContext, budget);
|
|
1391
|
+
const progressElement = built.elements.find((e) => e.type === "progress");
|
|
1392
|
+
|
|
1393
|
+
expect(progressElement).toBeDefined();
|
|
1394
|
+
// Progress shows counts only
|
|
1395
|
+
expect(progressElement!.content).toContain("2/3");
|
|
1396
|
+
// Does NOT contain other story titles
|
|
1397
|
+
expect(progressElement!.content).not.toContain("Secret Story Alpha");
|
|
1398
|
+
expect(progressElement!.content).not.toContain("Secret Story Beta");
|
|
1399
|
+
expect(progressElement!.content).not.toContain("US-001");
|
|
1400
|
+
expect(progressElement!.content).not.toContain("US-002");
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
test("prior errors from other stories do not leak into current story context", async () => {
|
|
1404
|
+
const prd = createTestPRD([
|
|
1405
|
+
{
|
|
1406
|
+
id: "US-001",
|
|
1407
|
+
title: "Story with errors",
|
|
1408
|
+
description: "Has prior errors",
|
|
1409
|
+
acceptanceCriteria: ["AC1"],
|
|
1410
|
+
dependencies: [],
|
|
1411
|
+
priorErrors: ["LEAKED_ERROR: something broke in US-001"],
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
id: "US-002",
|
|
1415
|
+
title: "Clean story",
|
|
1416
|
+
description: "No errors here",
|
|
1417
|
+
acceptanceCriteria: ["AC1"],
|
|
1418
|
+
dependencies: [],
|
|
1419
|
+
},
|
|
1420
|
+
]);
|
|
1421
|
+
|
|
1422
|
+
const storyContext: StoryContext = {
|
|
1423
|
+
prd,
|
|
1424
|
+
currentStoryId: "US-002",
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
const budget: ContextBudget = {
|
|
1428
|
+
maxTokens: 50000,
|
|
1429
|
+
reservedForInstructions: 5000,
|
|
1430
|
+
availableForContext: 45000,
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
const built = await buildContext(storyContext, budget);
|
|
1434
|
+
const markdown = formatContextAsMarkdown(built);
|
|
1435
|
+
|
|
1436
|
+
expect(markdown).not.toContain("LEAKED_ERROR");
|
|
1437
|
+
expect(markdown).not.toContain("US-001");
|
|
1438
|
+
// No error section at all
|
|
1439
|
+
expect(markdown).not.toContain("## Prior Errors");
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
test("context elements only contain expected types for a story with no deps/errors", async () => {
|
|
1443
|
+
const prd = createTestPRD([
|
|
1444
|
+
{
|
|
1445
|
+
id: "US-001",
|
|
1446
|
+
title: "Solo story",
|
|
1447
|
+
description: "No dependencies",
|
|
1448
|
+
acceptanceCriteria: ["AC1"],
|
|
1449
|
+
dependencies: [],
|
|
1450
|
+
},
|
|
1451
|
+
]);
|
|
1452
|
+
|
|
1453
|
+
const storyContext: StoryContext = {
|
|
1454
|
+
prd,
|
|
1455
|
+
currentStoryId: "US-001",
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
const budget: ContextBudget = {
|
|
1459
|
+
maxTokens: 50000,
|
|
1460
|
+
reservedForInstructions: 5000,
|
|
1461
|
+
availableForContext: 45000,
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
const built = await buildContext(storyContext, budget);
|
|
1465
|
+
|
|
1466
|
+
// Should only have progress + current story (no deps, no errors, no files, no test-coverage without workdir)
|
|
1467
|
+
const types = built.elements.map((e) => e.type);
|
|
1468
|
+
expect(types).toContain("progress");
|
|
1469
|
+
expect(types).toContain("story");
|
|
1470
|
+
expect(types).not.toContain("dependency");
|
|
1471
|
+
expect(types).not.toContain("error");
|
|
1472
|
+
|
|
1473
|
+
// All story elements reference only US-001
|
|
1474
|
+
const storyElements = built.elements.filter((e) => e.storyId);
|
|
1475
|
+
for (const el of storyElements) {
|
|
1476
|
+
expect(el.storyId).toBe("US-001");
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
describe("context auto-detection (BUG-006)", () => {
|
|
1482
|
+
test("should auto-detect files when contextFiles is empty", async () => {
|
|
1483
|
+
// Create temp git repo
|
|
1484
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1485
|
+
|
|
1486
|
+
try {
|
|
1487
|
+
// Initialize git
|
|
1488
|
+
await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
|
|
1489
|
+
await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
|
|
1490
|
+
await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
|
|
1491
|
+
|
|
1492
|
+
// Create files matching story keywords
|
|
1493
|
+
await fs.mkdir(path.join(tempDir, "src/routing"), { recursive: true });
|
|
1494
|
+
await fs.writeFile(path.join(tempDir, "src/routing/router.ts"), "export class Router { /* routing logic */ }");
|
|
1495
|
+
await fs.writeFile(
|
|
1496
|
+
path.join(tempDir, "src/routing/chain.ts"),
|
|
1497
|
+
"export class RouterChain { /* chain logic */ }",
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
// Commit so git grep can find them
|
|
1501
|
+
await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
|
|
1502
|
+
await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
|
|
1503
|
+
|
|
1504
|
+
const prd = createTestPRD([
|
|
1505
|
+
{
|
|
1506
|
+
id: "US-001",
|
|
1507
|
+
title: "Fix routing chain bug",
|
|
1508
|
+
description: "Fix issue in router chain",
|
|
1509
|
+
acceptanceCriteria: ["Router chain works correctly"],
|
|
1510
|
+
// No contextFiles - should auto-detect
|
|
1511
|
+
},
|
|
1512
|
+
]);
|
|
1513
|
+
|
|
1514
|
+
const storyContext: StoryContext = {
|
|
1515
|
+
prd,
|
|
1516
|
+
currentStoryId: "US-001",
|
|
1517
|
+
workdir: tempDir,
|
|
1518
|
+
config: {
|
|
1519
|
+
context: {
|
|
1520
|
+
autoDetect: {
|
|
1521
|
+
enabled: true,
|
|
1522
|
+
maxFiles: 5,
|
|
1523
|
+
traceImports: false,
|
|
1524
|
+
},
|
|
1525
|
+
testCoverage: {
|
|
1526
|
+
enabled: false, // Disable to isolate auto-detect test
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1529
|
+
} as any,
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
const budget: ContextBudget = {
|
|
1533
|
+
maxTokens: 10000,
|
|
1534
|
+
reservedForInstructions: 1000,
|
|
1535
|
+
availableForContext: 9000,
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const built = await buildContext(storyContext, budget);
|
|
1539
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
1540
|
+
|
|
1541
|
+
// Should auto-detect routing files
|
|
1542
|
+
expect(fileElements.length).toBeGreaterThan(0);
|
|
1543
|
+
const filePaths = fileElements.map((e) => e.filePath);
|
|
1544
|
+
expect(filePaths).toContain("src/routing/router.ts");
|
|
1545
|
+
expect(filePaths).toContain("src/routing/chain.ts");
|
|
1546
|
+
} finally {
|
|
1547
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
test("should skip auto-detection when contextFiles is provided", async () => {
|
|
1552
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1553
|
+
|
|
1554
|
+
try {
|
|
1555
|
+
await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
|
|
1556
|
+
await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
|
|
1557
|
+
await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
|
|
1558
|
+
|
|
1559
|
+
await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
|
|
1560
|
+
await fs.writeFile(path.join(tempDir, "src/explicit.ts"), "export const explicit = true;");
|
|
1561
|
+
await fs.writeFile(path.join(tempDir, "src/routing.ts"), "export const routing = true;");
|
|
1562
|
+
|
|
1563
|
+
await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
|
|
1564
|
+
await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
|
|
1565
|
+
|
|
1566
|
+
const prd = createTestPRD([
|
|
1567
|
+
{
|
|
1568
|
+
id: "US-001",
|
|
1569
|
+
title: "Fix routing bug",
|
|
1570
|
+
description: "Fix routing",
|
|
1571
|
+
acceptanceCriteria: ["Works"],
|
|
1572
|
+
contextFiles: ["src/explicit.ts"], // Explicit file provided
|
|
1573
|
+
},
|
|
1574
|
+
]);
|
|
1575
|
+
|
|
1576
|
+
const storyContext: StoryContext = {
|
|
1577
|
+
prd,
|
|
1578
|
+
currentStoryId: "US-001",
|
|
1579
|
+
workdir: tempDir,
|
|
1580
|
+
config: {
|
|
1581
|
+
context: {
|
|
1582
|
+
autoDetect: {
|
|
1583
|
+
enabled: true,
|
|
1584
|
+
maxFiles: 5,
|
|
1585
|
+
traceImports: false,
|
|
1586
|
+
},
|
|
1587
|
+
testCoverage: {
|
|
1588
|
+
enabled: false,
|
|
1589
|
+
},
|
|
1590
|
+
},
|
|
1591
|
+
} as any,
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
const budget: ContextBudget = {
|
|
1595
|
+
maxTokens: 10000,
|
|
1596
|
+
reservedForInstructions: 1000,
|
|
1597
|
+
availableForContext: 9000,
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
const built = await buildContext(storyContext, budget);
|
|
1601
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
1602
|
+
|
|
1603
|
+
// Should only load explicit file, NOT auto-detect
|
|
1604
|
+
expect(fileElements.length).toBe(1);
|
|
1605
|
+
expect(fileElements[0].filePath).toBe("src/explicit.ts");
|
|
1606
|
+
} finally {
|
|
1607
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
test("should skip auto-detection when disabled in config", async () => {
|
|
1612
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1613
|
+
|
|
1614
|
+
try {
|
|
1615
|
+
await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
|
|
1616
|
+
await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
|
|
1617
|
+
await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
|
|
1618
|
+
|
|
1619
|
+
await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
|
|
1620
|
+
await fs.writeFile(path.join(tempDir, "src/routing.ts"), "export const routing = true;");
|
|
1621
|
+
|
|
1622
|
+
await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
|
|
1623
|
+
await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
|
|
1624
|
+
|
|
1625
|
+
const prd = createTestPRD([
|
|
1626
|
+
{
|
|
1627
|
+
id: "US-001",
|
|
1628
|
+
title: "Fix routing bug",
|
|
1629
|
+
description: "Fix routing",
|
|
1630
|
+
acceptanceCriteria: ["Works"],
|
|
1631
|
+
// No contextFiles
|
|
1632
|
+
},
|
|
1633
|
+
]);
|
|
1634
|
+
|
|
1635
|
+
const storyContext: StoryContext = {
|
|
1636
|
+
prd,
|
|
1637
|
+
currentStoryId: "US-001",
|
|
1638
|
+
workdir: tempDir,
|
|
1639
|
+
config: {
|
|
1640
|
+
context: {
|
|
1641
|
+
autoDetect: {
|
|
1642
|
+
enabled: false, // Disabled
|
|
1643
|
+
maxFiles: 5,
|
|
1644
|
+
traceImports: false,
|
|
1645
|
+
},
|
|
1646
|
+
testCoverage: {
|
|
1647
|
+
enabled: false,
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
} as any,
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
const budget: ContextBudget = {
|
|
1654
|
+
maxTokens: 10000,
|
|
1655
|
+
reservedForInstructions: 1000,
|
|
1656
|
+
availableForContext: 9000,
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const built = await buildContext(storyContext, budget);
|
|
1660
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
1661
|
+
|
|
1662
|
+
// Should NOT auto-detect when disabled
|
|
1663
|
+
expect(fileElements.length).toBe(0);
|
|
1664
|
+
} finally {
|
|
1665
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
test("should handle auto-detection failure gracefully", async () => {
|
|
1670
|
+
// Non-git directory - git grep will fail
|
|
1671
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
1672
|
+
|
|
1673
|
+
try {
|
|
1674
|
+
await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
|
|
1675
|
+
await fs.writeFile(path.join(tempDir, "src/file.ts"), "export const test = true;");
|
|
1676
|
+
|
|
1677
|
+
const prd = createTestPRD([
|
|
1678
|
+
{
|
|
1679
|
+
id: "US-001",
|
|
1680
|
+
title: "Fix test bug",
|
|
1681
|
+
description: "Fix test",
|
|
1682
|
+
acceptanceCriteria: ["Works"],
|
|
1683
|
+
// No contextFiles
|
|
1684
|
+
},
|
|
1685
|
+
]);
|
|
1686
|
+
|
|
1687
|
+
const storyContext: StoryContext = {
|
|
1688
|
+
prd,
|
|
1689
|
+
currentStoryId: "US-001",
|
|
1690
|
+
workdir: tempDir,
|
|
1691
|
+
config: {
|
|
1692
|
+
context: {
|
|
1693
|
+
autoDetect: {
|
|
1694
|
+
enabled: true,
|
|
1695
|
+
maxFiles: 5,
|
|
1696
|
+
traceImports: false,
|
|
1697
|
+
},
|
|
1698
|
+
testCoverage: {
|
|
1699
|
+
enabled: false,
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1702
|
+
} as any,
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
const budget: ContextBudget = {
|
|
1706
|
+
maxTokens: 10000,
|
|
1707
|
+
reservedForInstructions: 1000,
|
|
1708
|
+
availableForContext: 9000,
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
// Should not throw, just log warning and continue
|
|
1712
|
+
const built = await buildContext(storyContext, budget);
|
|
1713
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
1714
|
+
|
|
1715
|
+
// No files loaded (graceful failure)
|
|
1716
|
+
expect(fileElements.length).toBe(0);
|
|
1717
|
+
} finally {
|
|
1718
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
});
|
|
1722
|
+
});
|