@nathapp/nax 0.20.0 → 0.22.0
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/.claude/settings.json +15 -0
- package/.mcp.json +8 -0
- package/docs/20260304-review-nax.md +492 -0
- package/docs/ROADMAP.md +65 -18
- package/docs/adr/ADR-005-implementation-plan.md +655 -0
- package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
- package/docs/specs/bug-039-orphan-processes.md +131 -0
- package/docs/specs/bug-040-review-rectification.md +82 -0
- package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
- package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
- package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
- package/docs/specs/feat-011-file-context-strategy.md +73 -0
- package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
- package/docs/specs/feat-013-test-after-review.md +89 -0
- package/docs/specs/feat-014-heartbeat-observability.md +127 -0
- package/memory/topic/feat-010-baseref.md +28 -0
- package/memory/topic/feat-013-test-after-deprecation.md +22 -0
- package/nax/config.json +7 -4
- package/nax/features/bug-039-medium/prd.json +45 -0
- package/package.json +2 -2
- package/src/agents/claude.ts +109 -15
- package/src/config/types.ts +11 -0
- package/src/context/builder.ts +9 -1
- package/src/execution/dry-run.ts +81 -0
- package/src/execution/escalation/tier-outcome.ts +29 -44
- package/src/execution/executor-types.ts +65 -0
- package/src/execution/index.ts +0 -17
- package/src/execution/iteration-runner.ts +132 -0
- package/src/execution/lifecycle/index.ts +0 -1
- package/src/execution/lifecycle/run-regression.ts +5 -5
- package/src/execution/pipeline-result-handler.ts +51 -254
- package/src/execution/sequential-executor.ts +72 -315
- package/src/execution/story-selector.ts +75 -0
- package/src/pipeline/event-bus.ts +276 -0
- package/src/pipeline/runner.ts +51 -77
- package/src/pipeline/stages/autofix.ts +133 -0
- package/src/pipeline/stages/completion.ts +22 -30
- package/src/pipeline/stages/index.ts +30 -13
- package/src/pipeline/stages/rectify.ts +93 -0
- package/src/pipeline/stages/regression.ts +88 -0
- package/src/pipeline/stages/review.ts +19 -153
- package/src/pipeline/stages/verify.ts +19 -3
- package/src/pipeline/subscribers/hooks.ts +133 -0
- package/src/pipeline/subscribers/interaction.ts +68 -0
- package/src/pipeline/subscribers/reporters.ts +174 -0
- package/src/pipeline/types.ts +12 -1
- package/src/review/orchestrator.ts +105 -0
- package/src/review/runner.ts +39 -4
- package/src/routing/router.ts +3 -3
- package/src/routing/strategies/keyword.ts +5 -2
- package/src/routing/strategies/llm.ts +27 -1
- package/src/tdd/prompts.ts +1 -1
- package/src/utils/git.ts +49 -25
- package/src/verification/executor.ts +8 -2
- package/src/verification/index.ts +1 -1
- package/src/verification/orchestrator-types.ts +145 -0
- package/src/verification/orchestrator.ts +76 -0
- package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
- package/src/verification/{gate.ts → runners.ts} +17 -105
- package/src/verification/smart-runner.ts +6 -10
- package/src/verification/strategies/acceptance.ts +133 -0
- package/src/verification/strategies/regression.ts +90 -0
- package/src/verification/strategies/scoped.ts +123 -0
- package/test/COVERAGE-GAPS.md +333 -0
- package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
- package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
- package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
- package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
- package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
- package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
- package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
- package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
- package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
- package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
- package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
- package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
- package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
- package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
- package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
- package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
- package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
- package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
- package/test/integration/config/merger.test.ts +1 -0
- package/test/integration/config/paths.test.ts +1 -0
- package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
- package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
- package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
- package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
- package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
- package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
- package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
- package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
- package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
- package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
- package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
- package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
- package/test/integration/execution/runner-batching.test.ts +682 -0
- package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
- package/test/integration/execution/runner-escalation.test.ts +561 -0
- package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
- package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
- package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
- package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
- package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
- package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
- package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
- package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
- package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
- package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
- package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
- package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
- package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
- package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
- package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
- package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
- package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
- package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
- package/test/integration/plugins/config-integration.test.ts +1 -0
- package/test/integration/plugins/config-resolution.test.ts +1 -0
- package/test/integration/plugins/loader.test.ts +1 -0
- package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
- package/test/integration/plugins/validator.test.ts +1 -0
- package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
- package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
- package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
- package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
- package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
- package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +10 -404
- package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
- package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
- package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
- package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
- package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
- package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
- package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
- package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
- package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
- package/test/integration/tmp/headless-test/test.jsonl +30 -0
- package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
- package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
- package/test/unit/acceptance.test.ts +1 -0
- package/test/unit/agent-stderr-capture.test.ts +1 -0
- package/test/unit/agents/claude.test.ts +107 -0
- package/test/unit/analyze-classifier.test.ts +1 -0
- package/test/unit/auto-detect.test.ts +1 -0
- package/test/unit/cli-status.test.ts +1 -0
- package/test/unit/commands/common.test.ts +1 -0
- package/test/unit/commands/logs.test.ts +1 -0
- package/test/unit/commands/unlock.test.ts +1 -0
- package/test/unit/config/defaults.test.ts +1 -0
- package/test/unit/config/regression-gate-schema.test.ts +1 -0
- package/test/unit/config/smart-runner-flag.test.ts +1 -0
- package/test/unit/constitution-generators.test.ts +1 -0
- package/test/unit/constitution.test.ts +1 -0
- package/test/unit/context/context-autodetect.test.ts +297 -0
- package/test/unit/context/context-build.test.ts +575 -0
- package/test/unit/context/context-coverage.test.ts +236 -0
- package/test/unit/context/context-error.test.ts +93 -0
- package/test/unit/context/context-estimate-tokens.test.ts +201 -0
- package/test/unit/context/context-format.test.ts +302 -0
- package/test/unit/context/context-isolation.test.ts +267 -0
- package/test/unit/context/context-sort.test.ts +93 -0
- package/test/unit/context/context-story.test.ts +108 -0
- package/test/{context → unit/context}/prior-failures.test.ts +5 -4
- package/test/unit/context.test.ts +7 -3
- package/test/unit/crash-recovery.test.ts +1 -0
- package/test/unit/escalation.test.ts +1 -0
- package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
- package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
- package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
- package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
- package/test/unit/execution-logging-stderr.test.ts +1 -0
- package/test/unit/execution-stage.test.ts +1 -0
- package/test/unit/fix-generator.test.ts +1 -0
- package/test/unit/greenfield.test.ts +1 -0
- package/test/unit/interaction/human-review-trigger.test.ts +1 -0
- package/test/unit/interaction-network-failures.test.ts +1 -0
- package/test/unit/interaction-plugins.test.ts +1 -0
- package/test/unit/logging/formatter.test.ts +1 -0
- package/test/unit/merge.test.ts +1 -0
- package/test/unit/pipeline/event-bus.test.ts +105 -0
- package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
- package/test/unit/pipeline/runner-retry.test.ts +89 -0
- package/test/unit/pipeline/stages/autofix.test.ts +97 -0
- package/test/unit/pipeline/stages/rectify.test.ts +101 -0
- package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
- package/test/unit/pipeline/stages/verify.test.ts +1 -0
- package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
- package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
- package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
- package/test/unit/pipeline/verify-smart-runner.test.ts +2 -1
- package/test/unit/prd-auto-default.test.ts +3 -2
- package/test/unit/prd-failure-category.test.ts +1 -0
- package/test/unit/prd-get-next-story.test.ts +1 -0
- package/test/unit/precheck-checks.test.ts +1 -0
- package/test/unit/precheck-story-size-gate.test.ts +1 -0
- package/test/unit/precheck-types.test.ts +1 -0
- package/test/unit/prompts.test.ts +1 -0
- package/test/unit/rectification.test.ts +2 -1
- package/test/unit/registry.test.ts +1 -0
- package/test/unit/routing/routing-stability.test.ts +2 -1
- package/test/unit/routing/strategies/llm.test.ts +251 -0
- package/test/unit/routing-advanced.test.ts +313 -0
- package/test/unit/routing-core.test.ts +341 -0
- package/test/unit/routing-strategies.test.ts +442 -0
- package/test/unit/storyid-events.test.ts +1 -0
- package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
- package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
- package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
- package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
- package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
- package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
- package/test/unit/verdict.test.ts +1 -0
- package/test/unit/verification/orchestrator-types.test.ts +54 -0
- package/test/unit/verification/orchestrator.test.ts +66 -0
- package/test/unit/verification/smart-runner-config.test.ts +1 -0
- package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
- package/test/unit/verification/strategies/acceptance.test.ts +33 -0
- package/test/unit/verification/strategies/regression.test.ts +87 -0
- package/test/unit/verification/strategies/scoped.test.ts +100 -0
- package/test/unit/worktree-manager.test.ts +1 -0
- package/src/execution/lifecycle/story-hooks.ts +0 -38
- package/src/execution/post-verify.ts +0 -193
- package/src/execution/rectification.ts +0 -13
- package/src/execution/verification.ts +0 -72
- package/test/integration/rectification-flow.test.ts +0 -512
- package/test/integration/runner.test.ts +0 -1679
- package/test/integration/tdd-orchestrator.test.ts +0 -1762
- package/test/unit/execution/post-verify-regression.test.ts +0 -362
- package/test/unit/execution/post-verify.test.ts +0 -236
- package/test/unit/routing.test.ts +0 -1039
- /package/test/{integration → helpers}/helpers.test.ts +0 -0
- /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Tests for context builder module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// RE-ARCH: keep
|
|
7
|
+
/**
|
|
8
|
+
* Tests for context builder module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
buildContext,
|
|
17
|
+
createDependencyContext,
|
|
18
|
+
createErrorContext,
|
|
19
|
+
createFileContext,
|
|
20
|
+
createProgressContext,
|
|
21
|
+
createStoryContext,
|
|
22
|
+
estimateTokens,
|
|
23
|
+
formatContextAsMarkdown,
|
|
24
|
+
sortContextElements,
|
|
25
|
+
} from "../../../src/context/builder";
|
|
26
|
+
import type { ContextBudget, ContextElement, StoryContext } from "../../../src/context/types";
|
|
27
|
+
import type { PRD, UserStory } from "../../../src/prd";
|
|
28
|
+
|
|
29
|
+
// Helper to create test PRD
|
|
30
|
+
const createTestPRD = (stories: Partial<UserStory>[]): PRD => ({
|
|
31
|
+
project: "test-project",
|
|
32
|
+
feature: "test-feature",
|
|
33
|
+
branchName: "test-branch",
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
userStories: stories.map((s, i) => ({
|
|
37
|
+
id: s.id || `US-${String(i + 1).padStart(3, "0")}`,
|
|
38
|
+
title: s.title || "Test Story",
|
|
39
|
+
description: s.description || "Test description",
|
|
40
|
+
acceptanceCriteria: s.acceptanceCriteria || ["AC1"],
|
|
41
|
+
dependencies: s.dependencies || [],
|
|
42
|
+
tags: s.tags || [],
|
|
43
|
+
status: s.status || "pending",
|
|
44
|
+
passes: s.passes ?? false,
|
|
45
|
+
escalations: s.escalations || [],
|
|
46
|
+
attempts: s.attempts || 0,
|
|
47
|
+
routing: s.routing,
|
|
48
|
+
priorErrors: s.priorErrors,
|
|
49
|
+
relevantFiles: s.relevantFiles,
|
|
50
|
+
contextFiles: s.contextFiles,
|
|
51
|
+
expectedFiles: s.expectedFiles,
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("Context Builder", () => {
|
|
56
|
+
describe("buildContext", () => {
|
|
57
|
+
test("should extract current story from PRD", async () => {
|
|
58
|
+
const prd = createTestPRD([
|
|
59
|
+
{
|
|
60
|
+
id: "US-001",
|
|
61
|
+
title: "First Story",
|
|
62
|
+
description: "First description",
|
|
63
|
+
acceptanceCriteria: ["AC1"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "US-002",
|
|
67
|
+
title: "Second Story",
|
|
68
|
+
description: "Second description",
|
|
69
|
+
acceptanceCriteria: ["AC2"],
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const storyContext: StoryContext = {
|
|
74
|
+
prd,
|
|
75
|
+
currentStoryId: "US-001",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const budget: ContextBudget = {
|
|
79
|
+
maxTokens: 10000,
|
|
80
|
+
reservedForInstructions: 1000,
|
|
81
|
+
availableForContext: 9000,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const built = await buildContext(storyContext, budget);
|
|
85
|
+
|
|
86
|
+
// Should have progress + current story
|
|
87
|
+
expect(built.elements.length).toBe(2);
|
|
88
|
+
expect(built.elements.some((e) => e.type === "progress")).toBe(true);
|
|
89
|
+
expect(built.elements.some((e) => e.type === "story" && e.storyId === "US-001")).toBe(true);
|
|
90
|
+
expect(built.totalTokens).toBeLessThanOrEqual(9000);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("should include dependency stories", async () => {
|
|
94
|
+
const prd = createTestPRD([
|
|
95
|
+
{
|
|
96
|
+
id: "US-001",
|
|
97
|
+
title: "Dependency Story",
|
|
98
|
+
description: "Dependency description",
|
|
99
|
+
acceptanceCriteria: ["AC1"],
|
|
100
|
+
status: "passed",
|
|
101
|
+
passes: true,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "US-002",
|
|
105
|
+
title: "Current Story",
|
|
106
|
+
description: "Current description",
|
|
107
|
+
acceptanceCriteria: ["AC2"],
|
|
108
|
+
dependencies: ["US-001"],
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const storyContext: StoryContext = {
|
|
113
|
+
prd,
|
|
114
|
+
currentStoryId: "US-002",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const budget: ContextBudget = {
|
|
118
|
+
maxTokens: 10000,
|
|
119
|
+
reservedForInstructions: 1000,
|
|
120
|
+
availableForContext: 9000,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const built = await buildContext(storyContext, budget);
|
|
124
|
+
|
|
125
|
+
// Should have progress + current story + dependency
|
|
126
|
+
expect(built.elements.length).toBe(3);
|
|
127
|
+
expect(built.elements.some((e) => e.type === "progress")).toBe(true);
|
|
128
|
+
expect(built.elements.some((e) => e.type === "story" && e.storyId === "US-002")).toBe(true);
|
|
129
|
+
expect(built.elements.some((e) => e.type === "dependency" && e.storyId === "US-001")).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("should include prior errors", async () => {
|
|
133
|
+
const prd = createTestPRD([
|
|
134
|
+
{
|
|
135
|
+
id: "US-001",
|
|
136
|
+
title: "Failed Story",
|
|
137
|
+
description: "Story with errors",
|
|
138
|
+
acceptanceCriteria: ["AC1"],
|
|
139
|
+
priorErrors: ["Error 1", "Error 2"],
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const storyContext: StoryContext = {
|
|
144
|
+
prd,
|
|
145
|
+
currentStoryId: "US-001",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const budget: ContextBudget = {
|
|
149
|
+
maxTokens: 10000,
|
|
150
|
+
reservedForInstructions: 1000,
|
|
151
|
+
availableForContext: 9000,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const built = await buildContext(storyContext, budget);
|
|
155
|
+
|
|
156
|
+
const errorElements = built.elements.filter((e) => e.type === "error");
|
|
157
|
+
expect(errorElements.length).toBe(2);
|
|
158
|
+
expect(built.summary).toContain("2 errors");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("should generate progress summary", async () => {
|
|
162
|
+
const prd = createTestPRD([
|
|
163
|
+
{ id: "US-001", status: "passed", passes: true },
|
|
164
|
+
{ id: "US-002", status: "passed", passes: true },
|
|
165
|
+
{ id: "US-003", status: "failed", passes: false },
|
|
166
|
+
{ id: "US-004", status: "pending", passes: false },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const storyContext: StoryContext = {
|
|
170
|
+
prd,
|
|
171
|
+
currentStoryId: "US-004",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const budget: ContextBudget = {
|
|
175
|
+
maxTokens: 10000,
|
|
176
|
+
reservedForInstructions: 1000,
|
|
177
|
+
availableForContext: 9000,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const built = await buildContext(storyContext, budget);
|
|
181
|
+
|
|
182
|
+
const progressElement = built.elements.find((e) => e.type === "progress");
|
|
183
|
+
expect(progressElement).toBeDefined();
|
|
184
|
+
expect(progressElement!.content).toContain("3/4 stories complete");
|
|
185
|
+
expect(progressElement!.content).toContain("2 passed");
|
|
186
|
+
expect(progressElement!.content).toContain("1 failed");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("should truncate when exceeding budget", async () => {
|
|
190
|
+
const prd = createTestPRD([
|
|
191
|
+
{
|
|
192
|
+
id: "US-001",
|
|
193
|
+
title: "Story with many dependencies",
|
|
194
|
+
description: "x".repeat(1000),
|
|
195
|
+
acceptanceCriteria: ["AC1"],
|
|
196
|
+
dependencies: ["US-002", "US-003", "US-004", "US-005"],
|
|
197
|
+
},
|
|
198
|
+
{ id: "US-002", description: "x".repeat(1000), acceptanceCriteria: ["AC2"] },
|
|
199
|
+
{ id: "US-003", description: "x".repeat(1000), acceptanceCriteria: ["AC3"] },
|
|
200
|
+
{ id: "US-004", description: "x".repeat(1000), acceptanceCriteria: ["AC4"] },
|
|
201
|
+
{ id: "US-005", description: "x".repeat(1000), acceptanceCriteria: ["AC5"] },
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const storyContext: StoryContext = {
|
|
205
|
+
prd,
|
|
206
|
+
currentStoryId: "US-001",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const budget: ContextBudget = {
|
|
210
|
+
maxTokens: 1000,
|
|
211
|
+
reservedForInstructions: 500,
|
|
212
|
+
availableForContext: 500, // Small budget
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const built = await buildContext(storyContext, budget);
|
|
216
|
+
|
|
217
|
+
expect(built.truncated).toBe(true);
|
|
218
|
+
expect(built.totalTokens).toBeLessThanOrEqual(500);
|
|
219
|
+
expect(built.summary).toContain("[TRUNCATED]");
|
|
220
|
+
// Progress should always be included (highest priority)
|
|
221
|
+
expect(built.elements.some((e) => e.type === "progress")).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("should throw error for non-existent story", async () => {
|
|
225
|
+
const prd = createTestPRD([{ id: "US-001", title: "Story" }]);
|
|
226
|
+
|
|
227
|
+
const storyContext: StoryContext = {
|
|
228
|
+
prd,
|
|
229
|
+
currentStoryId: "US-999", // Non-existent
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const budget: ContextBudget = {
|
|
233
|
+
maxTokens: 10000,
|
|
234
|
+
reservedForInstructions: 1000,
|
|
235
|
+
availableForContext: 9000,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
await expect(buildContext(storyContext, budget)).rejects.toThrow("Story US-999 not found in PRD");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("should load files from contextFiles when present", async () => {
|
|
242
|
+
// Create temp directory and files
|
|
243
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
244
|
+
const testFile1 = path.join(tempDir, "helper.ts");
|
|
245
|
+
const testFile2 = path.join(tempDir, "utils.ts");
|
|
246
|
+
|
|
247
|
+
await fs.writeFile(testFile1, 'export function helper() { return "test"; }');
|
|
248
|
+
await fs.writeFile(testFile2, 'export function utils() { return "util"; }');
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const prd = createTestPRD([
|
|
252
|
+
{
|
|
253
|
+
id: "US-001",
|
|
254
|
+
title: "Story with Files",
|
|
255
|
+
description: "Test",
|
|
256
|
+
acceptanceCriteria: ["AC1"],
|
|
257
|
+
contextFiles: ["helper.ts", "utils.ts"],
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const storyContext: StoryContext = {
|
|
262
|
+
prd,
|
|
263
|
+
currentStoryId: "US-001",
|
|
264
|
+
workdir: tempDir,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const budget: ContextBudget = {
|
|
268
|
+
maxTokens: 10000,
|
|
269
|
+
reservedForInstructions: 1000,
|
|
270
|
+
availableForContext: 9000,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const built = await buildContext(storyContext, budget);
|
|
274
|
+
|
|
275
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
276
|
+
expect(fileElements.length).toBe(2);
|
|
277
|
+
expect(fileElements[0].filePath).toBe("helper.ts");
|
|
278
|
+
expect(fileElements[1].filePath).toBe("utils.ts");
|
|
279
|
+
expect(fileElements[0].content).toContain("helper()");
|
|
280
|
+
expect(fileElements[1].content).toContain("utils()");
|
|
281
|
+
expect(built.summary).toContain("2 files");
|
|
282
|
+
} finally {
|
|
283
|
+
// Cleanup
|
|
284
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("should fall back to relevantFiles for file loading when contextFiles not present", async () => {
|
|
289
|
+
// Create temp directory and files
|
|
290
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
291
|
+
const testFile = path.join(tempDir, "legacy.ts");
|
|
292
|
+
|
|
293
|
+
await fs.writeFile(testFile, 'export function legacy() { return "old"; }');
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const prd = createTestPRD([
|
|
297
|
+
{
|
|
298
|
+
id: "US-001",
|
|
299
|
+
title: "Legacy Story with relevantFiles",
|
|
300
|
+
description: "Test backward compatibility",
|
|
301
|
+
acceptanceCriteria: ["AC1"],
|
|
302
|
+
relevantFiles: ["legacy.ts"],
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const storyContext: StoryContext = {
|
|
307
|
+
prd,
|
|
308
|
+
currentStoryId: "US-001",
|
|
309
|
+
workdir: tempDir,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const budget: ContextBudget = {
|
|
313
|
+
maxTokens: 10000,
|
|
314
|
+
reservedForInstructions: 1000,
|
|
315
|
+
availableForContext: 9000,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const built = await buildContext(storyContext, budget);
|
|
319
|
+
|
|
320
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
321
|
+
expect(fileElements.length).toBe(1);
|
|
322
|
+
expect(fileElements[0].filePath).toBe("legacy.ts");
|
|
323
|
+
expect(fileElements[0].content).toContain("legacy()");
|
|
324
|
+
} finally {
|
|
325
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("should prefer contextFiles over relevantFiles for file loading", async () => {
|
|
330
|
+
// Create temp directory and files
|
|
331
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
332
|
+
const newFile = path.join(tempDir, "new.ts");
|
|
333
|
+
const oldFile = path.join(tempDir, "old.ts");
|
|
334
|
+
|
|
335
|
+
await fs.writeFile(newFile, "export function newFunc() {}");
|
|
336
|
+
await fs.writeFile(oldFile, "export function oldFunc() {}");
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const prd = createTestPRD([
|
|
340
|
+
{
|
|
341
|
+
id: "US-001",
|
|
342
|
+
title: "Story with both contextFiles and relevantFiles",
|
|
343
|
+
description: "Test precedence",
|
|
344
|
+
acceptanceCriteria: ["AC1"],
|
|
345
|
+
contextFiles: ["new.ts"],
|
|
346
|
+
relevantFiles: ["old.ts"],
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const storyContext: StoryContext = {
|
|
351
|
+
prd,
|
|
352
|
+
currentStoryId: "US-001",
|
|
353
|
+
workdir: tempDir,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const budget: ContextBudget = {
|
|
357
|
+
maxTokens: 10000,
|
|
358
|
+
reservedForInstructions: 1000,
|
|
359
|
+
availableForContext: 9000,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const built = await buildContext(storyContext, budget);
|
|
363
|
+
|
|
364
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
365
|
+
expect(fileElements.length).toBe(1);
|
|
366
|
+
expect(fileElements[0].filePath).toBe("new.ts");
|
|
367
|
+
expect(fileElements[0].content).toContain("newFunc()");
|
|
368
|
+
// Should NOT load old.ts
|
|
369
|
+
expect(fileElements.find((e) => e.filePath === "old.ts")).toBeUndefined();
|
|
370
|
+
} finally {
|
|
371
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("should respect max 5 files limit", async () => {
|
|
376
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
// Create 10 test files
|
|
380
|
+
const files: string[] = [];
|
|
381
|
+
for (let i = 0; i < 10; i++) {
|
|
382
|
+
const filename = `file${i}.ts`;
|
|
383
|
+
files.push(filename);
|
|
384
|
+
await fs.writeFile(path.join(tempDir, filename), `export const file${i} = ${i};`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const prd = createTestPRD([
|
|
388
|
+
{
|
|
389
|
+
id: "US-001",
|
|
390
|
+
title: "Story with Many Files",
|
|
391
|
+
description: "Test",
|
|
392
|
+
acceptanceCriteria: ["AC1"],
|
|
393
|
+
contextFiles: files,
|
|
394
|
+
},
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
const storyContext: StoryContext = {
|
|
398
|
+
prd,
|
|
399
|
+
currentStoryId: "US-001",
|
|
400
|
+
workdir: tempDir,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const budget: ContextBudget = {
|
|
404
|
+
maxTokens: 10000,
|
|
405
|
+
reservedForInstructions: 1000,
|
|
406
|
+
availableForContext: 9000,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const built = await buildContext(storyContext, budget);
|
|
410
|
+
|
|
411
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
412
|
+
expect(fileElements.length).toBe(5); // Max 5 files
|
|
413
|
+
} finally {
|
|
414
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("should add path-only element for files larger than 10KB", async () => {
|
|
419
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const smallFile = path.join(tempDir, "small.ts");
|
|
423
|
+
const largeFile = path.join(tempDir, "large.ts");
|
|
424
|
+
|
|
425
|
+
await fs.writeFile(smallFile, 'export const small = "ok";');
|
|
426
|
+
await fs.writeFile(largeFile, "x".repeat(11 * 1024)); // 11KB
|
|
427
|
+
|
|
428
|
+
const prd = createTestPRD([
|
|
429
|
+
{
|
|
430
|
+
id: "US-001",
|
|
431
|
+
title: "Story with Large File",
|
|
432
|
+
description: "Test",
|
|
433
|
+
acceptanceCriteria: ["AC1"],
|
|
434
|
+
contextFiles: ["small.ts", "large.ts"],
|
|
435
|
+
},
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
const storyContext: StoryContext = {
|
|
439
|
+
prd,
|
|
440
|
+
currentStoryId: "US-001",
|
|
441
|
+
workdir: tempDir,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const budget: ContextBudget = {
|
|
445
|
+
maxTokens: 20000,
|
|
446
|
+
reservedForInstructions: 1000,
|
|
447
|
+
availableForContext: 19000,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Capture warnings
|
|
451
|
+
const originalWarn = console.warn;
|
|
452
|
+
const warnings: string[] = [];
|
|
453
|
+
console.warn = (msg: string) => warnings.push(msg);
|
|
454
|
+
|
|
455
|
+
const built = await buildContext(storyContext, budget);
|
|
456
|
+
|
|
457
|
+
console.warn = originalWarn;
|
|
458
|
+
|
|
459
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
460
|
+
expect(fileElements.length).toBe(2); // small file (inline) + large file (path-only)
|
|
461
|
+
expect(fileElements[0].filePath).toBe("small.ts");
|
|
462
|
+
// Large file gets path-only hint (FEAT-011)
|
|
463
|
+
const largeElement = fileElements.find((e) => e.filePath === "large.ts");
|
|
464
|
+
expect(largeElement).toBeDefined();
|
|
465
|
+
expect(largeElement!.content).toContain("File too large to inline");
|
|
466
|
+
} finally {
|
|
467
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("should warn on missing files", async () => {
|
|
472
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const prd = createTestPRD([
|
|
476
|
+
{
|
|
477
|
+
id: "US-001",
|
|
478
|
+
title: "Story with Missing File",
|
|
479
|
+
description: "Test",
|
|
480
|
+
acceptanceCriteria: ["AC1"],
|
|
481
|
+
contextFiles: ["nonexistent.ts"],
|
|
482
|
+
},
|
|
483
|
+
]);
|
|
484
|
+
|
|
485
|
+
const storyContext: StoryContext = {
|
|
486
|
+
prd,
|
|
487
|
+
currentStoryId: "US-001",
|
|
488
|
+
workdir: tempDir,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const budget: ContextBudget = {
|
|
492
|
+
maxTokens: 10000,
|
|
493
|
+
reservedForInstructions: 1000,
|
|
494
|
+
availableForContext: 9000,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const built = await buildContext(storyContext, budget);
|
|
498
|
+
|
|
499
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
500
|
+
expect(fileElements.length).toBe(0);
|
|
501
|
+
// Missing file should be skipped (warning logged via structured logger)
|
|
502
|
+
} finally {
|
|
503
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("should handle empty contextFiles array", async () => {
|
|
508
|
+
const prd = createTestPRD([
|
|
509
|
+
{
|
|
510
|
+
id: "US-001",
|
|
511
|
+
title: "Story with Empty Files",
|
|
512
|
+
description: "Test",
|
|
513
|
+
acceptanceCriteria: ["AC1"],
|
|
514
|
+
contextFiles: [],
|
|
515
|
+
},
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
const storyContext: StoryContext = {
|
|
519
|
+
prd,
|
|
520
|
+
currentStoryId: "US-001",
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const budget: ContextBudget = {
|
|
524
|
+
maxTokens: 10000,
|
|
525
|
+
reservedForInstructions: 1000,
|
|
526
|
+
availableForContext: 9000,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const built = await buildContext(storyContext, budget);
|
|
530
|
+
|
|
531
|
+
const fileElements = built.elements.filter((e) => e.type === "file");
|
|
532
|
+
expect(fileElements.length).toBe(0);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("should respect token budget when loading files", async () => {
|
|
536
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
// Create files with substantial content
|
|
540
|
+
await fs.writeFile(path.join(tempDir, "file1.ts"), "x".repeat(5000));
|
|
541
|
+
await fs.writeFile(path.join(tempDir, "file2.ts"), "x".repeat(5000));
|
|
542
|
+
|
|
543
|
+
const prd = createTestPRD([
|
|
544
|
+
{
|
|
545
|
+
id: "US-001",
|
|
546
|
+
title: "Story",
|
|
547
|
+
description: "x".repeat(1000),
|
|
548
|
+
acceptanceCriteria: ["AC1"],
|
|
549
|
+
contextFiles: ["file1.ts", "file2.ts"],
|
|
550
|
+
},
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
const storyContext: StoryContext = {
|
|
554
|
+
prd,
|
|
555
|
+
currentStoryId: "US-001",
|
|
556
|
+
workdir: tempDir,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const budget: ContextBudget = {
|
|
560
|
+
maxTokens: 2000,
|
|
561
|
+
reservedForInstructions: 500,
|
|
562
|
+
availableForContext: 1500, // Small budget
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const built = await buildContext(storyContext, budget);
|
|
566
|
+
|
|
567
|
+
expect(built.totalTokens).toBeLessThanOrEqual(1500);
|
|
568
|
+
// Files have lower priority (60) than story (80), so story should be included
|
|
569
|
+
expect(built.elements.some((e) => e.type === "story")).toBe(true);
|
|
570
|
+
} finally {
|
|
571
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
});
|