@nathapp/nax 0.21.0 → 0.22.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/.mcp.json +8 -0
- package/docs/ROADMAP.md +20 -5
- package/docs/adr/ADR-005-implementation-plan.md +655 -0
- package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
- package/package.json +1 -1
- package/src/agents/claude.ts +44 -9
- package/src/config/types.ts +11 -0
- 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 -316
- 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 +18 -2
- 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 +10 -1
- package/src/review/orchestrator.ts +105 -0
- package/src/tdd/prompts.ts +1 -1
- 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/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} +9 -403
- 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 +1 -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 +1 -0
- 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 +1 -0
- package/test/unit/prd-auto-default.test.ts +1 -0
- 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 +1 -0
- package/test/unit/routing/strategies/llm.test.ts +1 -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,108 @@
|
|
|
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("createStoryContext", () => {
|
|
57
|
+
test("should create story context element", () => {
|
|
58
|
+
const story: UserStory = {
|
|
59
|
+
id: "US-001",
|
|
60
|
+
title: "Test Story",
|
|
61
|
+
description: "Test description",
|
|
62
|
+
acceptanceCriteria: ["AC1", "AC2"],
|
|
63
|
+
dependencies: [],
|
|
64
|
+
tags: ["feature"],
|
|
65
|
+
status: "pending",
|
|
66
|
+
passes: false,
|
|
67
|
+
escalations: [],
|
|
68
|
+
attempts: 0,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const element = createStoryContext(story, 80);
|
|
72
|
+
|
|
73
|
+
expect(element.type).toBe("story");
|
|
74
|
+
expect(element.storyId).toBe("US-001");
|
|
75
|
+
expect(element.priority).toBe(80);
|
|
76
|
+
expect(element.content).toContain("US-001: Test Story");
|
|
77
|
+
expect(element.content).toContain("Test description");
|
|
78
|
+
expect(element.content).toContain("AC1");
|
|
79
|
+
expect(element.content).toContain("AC2");
|
|
80
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("createDependencyContext", () => {
|
|
85
|
+
test("should create dependency context element", () => {
|
|
86
|
+
const story: UserStory = {
|
|
87
|
+
id: "US-002",
|
|
88
|
+
title: "Dependency Story",
|
|
89
|
+
description: "Dependency description",
|
|
90
|
+
acceptanceCriteria: ["AC1"],
|
|
91
|
+
dependencies: [],
|
|
92
|
+
tags: [],
|
|
93
|
+
status: "passed",
|
|
94
|
+
passes: true,
|
|
95
|
+
escalations: [],
|
|
96
|
+
attempts: 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const element = createDependencyContext(story, 50);
|
|
100
|
+
|
|
101
|
+
expect(element.type).toBe("dependency");
|
|
102
|
+
expect(element.storyId).toBe("US-002");
|
|
103
|
+
expect(element.priority).toBe(50);
|
|
104
|
+
expect(element.content).toContain("US-002: Dependency Story");
|
|
105
|
+
expect(element.tokens).toBeGreaterThan(0);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for priorFailures context formatting
|
|
3
4
|
*
|
|
@@ -5,10 +6,10 @@
|
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import { createPriorFailuresContext, formatPriorFailures } from "
|
|
9
|
-
import { buildContext, sortContextElements } from "
|
|
10
|
-
import type { StructuredFailure, UserStory } from "
|
|
11
|
-
import type { StoryContext } from "
|
|
9
|
+
import { createPriorFailuresContext, formatPriorFailures } from "../../../src/context/elements";
|
|
10
|
+
import { buildContext, sortContextElements } from "../../../src/context/builder";
|
|
11
|
+
import type { StructuredFailure, UserStory } from "../../../src/prd";
|
|
12
|
+
import type { StoryContext } from "../../../src/context/types";
|
|
12
13
|
|
|
13
14
|
describe("formatPriorFailures", () => {
|
|
14
15
|
test("should format a single prior failure correctly", () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
1
2
|
/**
|
|
2
3
|
* PID Registry Tests
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
7
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
7
|
-
import { PidRegistry } from "
|
|
8
|
+
import { PidRegistry } from "../../../src/execution/pid-registry";
|
|
8
9
|
|
|
9
10
|
const TEST_WORKDIR = "/tmp/nax-pid-registry-test";
|
|
10
11
|
const PID_FILE = `${TEST_WORKDIR}/.nax-pids`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for StructuredFailure and priorFailures tracking
|
|
3
4
|
*
|
|
@@ -5,8 +6,8 @@
|
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import { loadPRD } from "
|
|
9
|
-
import type { StructuredFailure, TestFailureContext, UserStory } from "
|
|
9
|
+
import { loadPRD } from "../../../src/prd";
|
|
10
|
+
import type { StructuredFailure, TestFailureContext, UserStory } from "../../../src/prd";
|
|
10
11
|
|
|
11
12
|
describe("StructuredFailure Type", () => {
|
|
12
13
|
test("should have all required fields", () => {
|
package/test/unit/merge.test.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { PipelineEventBus } from "../../../src/pipeline/event-bus";
|
|
4
|
+
import type { PipelineEvent } from "../../../src/pipeline/event-bus";
|
|
5
|
+
|
|
6
|
+
function makeStoryCompletedEvent(): PipelineEvent {
|
|
7
|
+
return {
|
|
8
|
+
type: "story:completed",
|
|
9
|
+
storyId: "US-001",
|
|
10
|
+
story: { id: "US-001", title: "Test story", status: "passed", acceptanceCriteria: [] } as any,
|
|
11
|
+
passed: true,
|
|
12
|
+
durationMs: 1000,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("PipelineEventBus", () => {
|
|
17
|
+
test("subscribes and receives event", () => {
|
|
18
|
+
const bus = new PipelineEventBus();
|
|
19
|
+
const received: PipelineEvent[] = [];
|
|
20
|
+
bus.on("story:completed", (e) => received.push(e));
|
|
21
|
+
|
|
22
|
+
const evt = makeStoryCompletedEvent();
|
|
23
|
+
bus.emit(evt);
|
|
24
|
+
|
|
25
|
+
expect(received).toHaveLength(1);
|
|
26
|
+
expect(received[0]).toBe(evt);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("onAll receives all event types", () => {
|
|
30
|
+
const bus = new PipelineEventBus();
|
|
31
|
+
const received: string[] = [];
|
|
32
|
+
bus.onAll((e) => received.push(e.type));
|
|
33
|
+
|
|
34
|
+
bus.emit(makeStoryCompletedEvent());
|
|
35
|
+
bus.emit({ type: "run:completed", totalStories: 1, passedStories: 1, failedStories: 0, durationMs: 5000 });
|
|
36
|
+
|
|
37
|
+
expect(received).toEqual(["story:completed", "run:completed"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("unsubscribe stops receiving events", () => {
|
|
41
|
+
const bus = new PipelineEventBus();
|
|
42
|
+
const received: PipelineEvent[] = [];
|
|
43
|
+
const unsub = bus.on("story:completed", (e) => received.push(e));
|
|
44
|
+
|
|
45
|
+
bus.emit(makeStoryCompletedEvent());
|
|
46
|
+
unsub();
|
|
47
|
+
bus.emit(makeStoryCompletedEvent());
|
|
48
|
+
|
|
49
|
+
expect(received).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("subscriber error does not prevent other subscribers from running", () => {
|
|
53
|
+
const bus = new PipelineEventBus();
|
|
54
|
+
const results: string[] = [];
|
|
55
|
+
|
|
56
|
+
bus.on("story:completed", () => { throw new Error("boom"); });
|
|
57
|
+
bus.on("story:completed", () => results.push("second"));
|
|
58
|
+
|
|
59
|
+
bus.emit(makeStoryCompletedEvent());
|
|
60
|
+
|
|
61
|
+
expect(results).toEqual(["second"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("subscriberCount returns correct count", () => {
|
|
65
|
+
const bus = new PipelineEventBus();
|
|
66
|
+
expect(bus.subscriberCount("story:completed")).toBe(0);
|
|
67
|
+
|
|
68
|
+
bus.on("story:completed", () => {});
|
|
69
|
+
bus.on("story:completed", () => {});
|
|
70
|
+
expect(bus.subscriberCount("story:completed")).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("clear removes all subscribers", () => {
|
|
74
|
+
const bus = new PipelineEventBus();
|
|
75
|
+
const received: PipelineEvent[] = [];
|
|
76
|
+
bus.on("story:completed", (e) => received.push(e));
|
|
77
|
+
bus.clear();
|
|
78
|
+
bus.emit(makeStoryCompletedEvent());
|
|
79
|
+
|
|
80
|
+
expect(received).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("emitAsync awaits all async subscribers", async () => {
|
|
84
|
+
const bus = new PipelineEventBus();
|
|
85
|
+
let resolved = false;
|
|
86
|
+
|
|
87
|
+
bus.on("story:completed", async () => {
|
|
88
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
89
|
+
resolved = true;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await bus.emitAsync(makeStoryCompletedEvent());
|
|
93
|
+
expect(resolved).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("does not deliver typed events to wrong subscriber", () => {
|
|
97
|
+
const bus = new PipelineEventBus();
|
|
98
|
+
const received: PipelineEvent[] = [];
|
|
99
|
+
|
|
100
|
+
bus.on("run:completed", (e) => received.push(e));
|
|
101
|
+
bus.emit(makeStoryCompletedEvent());
|
|
102
|
+
|
|
103
|
+
expect(received).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { MAX_STAGE_RETRIES, runPipeline } from "../../../src/pipeline/runner";
|
|
4
|
+
import type { PipelineContext, PipelineStage } from "../../../src/pipeline/types";
|
|
5
|
+
import { DEFAULT_CONFIG } from "../../../src/config";
|
|
6
|
+
|
|
7
|
+
function makeCtx(): PipelineContext {
|
|
8
|
+
return {
|
|
9
|
+
config: DEFAULT_CONFIG,
|
|
10
|
+
prd: { stories: [], acceptanceOverrides: {} } as any,
|
|
11
|
+
story: { id: "US-001", title: "t", status: "pending", acceptanceCriteria: [] } as any,
|
|
12
|
+
stories: [],
|
|
13
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
|
|
14
|
+
workdir: "/tmp",
|
|
15
|
+
hooks: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stage(name: string, action: () => import("../../../src/pipeline/types").StageResult | Promise<import("../../../src/pipeline/types").StageResult>): PipelineStage {
|
|
20
|
+
return { name, enabled: () => true, execute: async () => action() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("runPipeline retry action", () => {
|
|
24
|
+
test("retry jumps back to named stage", async () => {
|
|
25
|
+
const order: string[] = [];
|
|
26
|
+
let attempt = 0;
|
|
27
|
+
|
|
28
|
+
const stages = [
|
|
29
|
+
stage("a", () => { order.push("a"); return { action: "continue" }; }),
|
|
30
|
+
stage("b", () => { order.push("b"); return { action: "continue" }; }),
|
|
31
|
+
stage("c", () => {
|
|
32
|
+
order.push("c");
|
|
33
|
+
attempt++;
|
|
34
|
+
if (attempt < 2) return { action: "retry", fromStage: "b" };
|
|
35
|
+
return { action: "continue" };
|
|
36
|
+
}),
|
|
37
|
+
stage("d", () => { order.push("d"); return { action: "continue" }; }),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const result = await runPipeline(stages, makeCtx());
|
|
41
|
+
|
|
42
|
+
expect(result.finalAction).toBe("complete");
|
|
43
|
+
expect(order).toEqual(["a", "b", "c", "b", "c", "d"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("retry fails after MAX_STAGE_RETRIES exceeded", async () => {
|
|
47
|
+
let calls = 0;
|
|
48
|
+
const stages = [
|
|
49
|
+
stage("verify", () => { return { action: "continue" }; }),
|
|
50
|
+
stage("rectify", () => { calls++; return { action: "retry", fromStage: "verify" }; }),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const result = await runPipeline(stages, makeCtx());
|
|
54
|
+
|
|
55
|
+
expect(result.finalAction).toBe("fail");
|
|
56
|
+
expect(calls).toBe(MAX_STAGE_RETRIES + 1);
|
|
57
|
+
expect(result.reason).toContain("exceeded max retries");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("retry to unknown stage escalates", async () => {
|
|
61
|
+
const stages = [
|
|
62
|
+
stage("a", () => ({ action: "retry", fromStage: "nonexistent" })),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const result = await runPipeline(stages, makeCtx());
|
|
66
|
+
expect(result.finalAction).toBe("escalate");
|
|
67
|
+
expect(result.reason).toContain("not found");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("disabled stages are skipped during retry", async () => {
|
|
71
|
+
const order: string[] = [];
|
|
72
|
+
let attempt = 0;
|
|
73
|
+
|
|
74
|
+
const stages = [
|
|
75
|
+
stage("verify", () => { order.push("verify"); return { action: "continue" }; }),
|
|
76
|
+
{ name: "disabled", enabled: () => false, execute: async () => { order.push("disabled"); return { action: "continue" as const }; } },
|
|
77
|
+
stage("rectify", () => {
|
|
78
|
+
order.push("rectify");
|
|
79
|
+
attempt++;
|
|
80
|
+
if (attempt < 2) return { action: "retry", fromStage: "verify" };
|
|
81
|
+
return { action: "continue" };
|
|
82
|
+
}),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
await runPipeline(stages, makeCtx());
|
|
86
|
+
expect(order).not.toContain("disabled");
|
|
87
|
+
expect(order).toEqual(["verify", "rectify", "verify", "rectify"]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { autofixStage, _autofixDeps } from "../../../../src/pipeline/stages/autofix";
|
|
4
|
+
import type { PipelineContext } from "../../../../src/pipeline/types";
|
|
5
|
+
import { DEFAULT_CONFIG } from "../../../../src/config";
|
|
6
|
+
|
|
7
|
+
function makeReviewResult(success: boolean) {
|
|
8
|
+
return { success, checks: [], summary: "" } as any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
12
|
+
return {
|
|
13
|
+
config: {
|
|
14
|
+
...DEFAULT_CONFIG,
|
|
15
|
+
quality: {
|
|
16
|
+
...DEFAULT_CONFIG.quality,
|
|
17
|
+
commands: {
|
|
18
|
+
...DEFAULT_CONFIG.quality.commands,
|
|
19
|
+
lintFix: "biome check --fix",
|
|
20
|
+
formatFix: "biome format --write",
|
|
21
|
+
},
|
|
22
|
+
autofix: { enabled: true, maxAttempts: 2 },
|
|
23
|
+
},
|
|
24
|
+
} as any,
|
|
25
|
+
prd: { stories: [] } as any,
|
|
26
|
+
story: { id: "US-001", title: "t", status: "in-progress", acceptanceCriteria: [] } as any,
|
|
27
|
+
stories: [],
|
|
28
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
|
|
29
|
+
workdir: "/tmp",
|
|
30
|
+
hooks: {},
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("autofixStage", () => {
|
|
36
|
+
test("disabled when reviewResult is undefined", () => {
|
|
37
|
+
expect(autofixStage.enabled(makeCtx())).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("disabled when review passed", () => {
|
|
41
|
+
expect(autofixStage.enabled(makeCtx({ reviewResult: makeReviewResult(true) }))).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("disabled when autofix.enabled = false", () => {
|
|
45
|
+
const ctx = makeCtx({
|
|
46
|
+
reviewResult: makeReviewResult(false),
|
|
47
|
+
config: {
|
|
48
|
+
...DEFAULT_CONFIG,
|
|
49
|
+
quality: { ...DEFAULT_CONFIG.quality, autofix: { enabled: false } },
|
|
50
|
+
} as any,
|
|
51
|
+
});
|
|
52
|
+
expect(autofixStage.enabled(ctx)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("escalates when no fix commands configured", async () => {
|
|
56
|
+
const ctx = makeCtx({
|
|
57
|
+
reviewResult: makeReviewResult(false),
|
|
58
|
+
config: {
|
|
59
|
+
...DEFAULT_CONFIG,
|
|
60
|
+
quality: {
|
|
61
|
+
...DEFAULT_CONFIG.quality,
|
|
62
|
+
commands: { test: "bun test" },
|
|
63
|
+
autofix: { enabled: true },
|
|
64
|
+
},
|
|
65
|
+
} as any,
|
|
66
|
+
});
|
|
67
|
+
const result = await autofixStage.execute(ctx);
|
|
68
|
+
expect(result.action).toBe("escalate");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns retry when recheck passes", async () => {
|
|
72
|
+
const saved = { ..._autofixDeps };
|
|
73
|
+
_autofixDeps.runCommand = async () => ({ exitCode: 0, output: "" });
|
|
74
|
+
_autofixDeps.recheckReview = async () => true;
|
|
75
|
+
|
|
76
|
+
const ctx = makeCtx({ reviewResult: makeReviewResult(false) });
|
|
77
|
+
const result = await autofixStage.execute(ctx);
|
|
78
|
+
|
|
79
|
+
Object.assign(_autofixDeps, saved);
|
|
80
|
+
|
|
81
|
+
expect(result.action).toBe("retry");
|
|
82
|
+
if (result.action === "retry") expect(result.fromStage).toBe("review");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("escalates when recheck still fails after max attempts", async () => {
|
|
86
|
+
const saved = { ..._autofixDeps };
|
|
87
|
+
_autofixDeps.runCommand = async () => ({ exitCode: 1, output: "lint error" });
|
|
88
|
+
_autofixDeps.recheckReview = async () => false;
|
|
89
|
+
|
|
90
|
+
const ctx = makeCtx({ reviewResult: makeReviewResult(false) });
|
|
91
|
+
const result = await autofixStage.execute(ctx);
|
|
92
|
+
|
|
93
|
+
Object.assign(_autofixDeps, saved);
|
|
94
|
+
|
|
95
|
+
expect(result.action).toBe("escalate");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { rectifyStage, _rectifyDeps } from "../../../../src/pipeline/stages/rectify";
|
|
4
|
+
import type { PipelineContext } from "../../../../src/pipeline/types";
|
|
5
|
+
import { DEFAULT_CONFIG } from "../../../../src/config";
|
|
6
|
+
|
|
7
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
8
|
+
return {
|
|
9
|
+
config: {
|
|
10
|
+
...DEFAULT_CONFIG,
|
|
11
|
+
execution: {
|
|
12
|
+
...DEFAULT_CONFIG.execution,
|
|
13
|
+
rectification: { enabled: true, maxRetries: 3, abortOnIncreasingFailures: true, maxFailureSummaryChars: 2000 },
|
|
14
|
+
},
|
|
15
|
+
quality: {
|
|
16
|
+
...DEFAULT_CONFIG.quality,
|
|
17
|
+
commands: { ...DEFAULT_CONFIG.quality.commands, test: "bun test" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
prd: { stories: [] } as any,
|
|
21
|
+
story: { id: "US-001", title: "t", status: "in-progress", acceptanceCriteria: [] } as any,
|
|
22
|
+
stories: [],
|
|
23
|
+
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
|
|
24
|
+
workdir: "/tmp",
|
|
25
|
+
hooks: {},
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeVerifyResult(success: boolean) {
|
|
31
|
+
return {
|
|
32
|
+
success,
|
|
33
|
+
status: success ? ("PASS" as const) : ("TEST_FAILURE" as const),
|
|
34
|
+
storyId: "US-001",
|
|
35
|
+
strategy: "scoped" as const,
|
|
36
|
+
passCount: success ? 10 : 8,
|
|
37
|
+
failCount: success ? 0 : 2,
|
|
38
|
+
totalCount: 10,
|
|
39
|
+
failures: [],
|
|
40
|
+
rawOutput: "(fail) foo > bar",
|
|
41
|
+
durationMs: 100,
|
|
42
|
+
countsTowardEscalation: !success,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("rectifyStage", () => {
|
|
47
|
+
test("disabled when verifyResult is undefined", () => {
|
|
48
|
+
expect(rectifyStage.enabled(makeCtx())).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("disabled when verify passed", () => {
|
|
52
|
+
const ctx = makeCtx({ verifyResult: makeVerifyResult(true) });
|
|
53
|
+
expect(rectifyStage.enabled(ctx)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("disabled when rectification config disabled", () => {
|
|
57
|
+
const ctx = makeCtx({
|
|
58
|
+
verifyResult: makeVerifyResult(false),
|
|
59
|
+
config: {
|
|
60
|
+
...DEFAULT_CONFIG,
|
|
61
|
+
execution: {
|
|
62
|
+
...DEFAULT_CONFIG.execution,
|
|
63
|
+
rectification: { enabled: false, maxRetries: 3, abortOnIncreasingFailures: true, maxFailureSummaryChars: 2000 },
|
|
64
|
+
},
|
|
65
|
+
} as any,
|
|
66
|
+
});
|
|
67
|
+
expect(rectifyStage.enabled(ctx)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("enabled when verify failed and rectification enabled", () => {
|
|
71
|
+
const ctx = makeCtx({ verifyResult: makeVerifyResult(false) });
|
|
72
|
+
expect(rectifyStage.enabled(ctx)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns retry when rectification succeeds", async () => {
|
|
76
|
+
const saved = { ..._rectifyDeps };
|
|
77
|
+
_rectifyDeps.runRectificationLoop = async () => true;
|
|
78
|
+
|
|
79
|
+
const ctx = makeCtx({ verifyResult: makeVerifyResult(false) });
|
|
80
|
+
const result = await rectifyStage.execute(ctx);
|
|
81
|
+
|
|
82
|
+
Object.assign(_rectifyDeps, saved);
|
|
83
|
+
|
|
84
|
+
expect(result.action).toBe("retry");
|
|
85
|
+
if (result.action === "retry") expect(result.fromStage).toBe("verify");
|
|
86
|
+
// verifyResult should be cleared so verify re-runs fresh
|
|
87
|
+
expect(ctx.verifyResult).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("returns escalate when rectification exhausted", async () => {
|
|
91
|
+
const saved = { ..._rectifyDeps };
|
|
92
|
+
_rectifyDeps.runRectificationLoop = async () => false;
|
|
93
|
+
|
|
94
|
+
const ctx = makeCtx({ verifyResult: makeVerifyResult(false) });
|
|
95
|
+
const result = await rectifyStage.execute(ctx);
|
|
96
|
+
|
|
97
|
+
Object.assign(_rectifyDeps, saved);
|
|
98
|
+
|
|
99
|
+
expect(result.action).toBe("escalate");
|
|
100
|
+
});
|
|
101
|
+
});
|