@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,461 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Plugin Routing Integration Tests
|
|
4
|
+
*
|
|
5
|
+
* Tests for US-005: Plugin routing strategies integrate into router chain
|
|
6
|
+
*
|
|
7
|
+
* Acceptance Criteria:
|
|
8
|
+
* 1. Plugin routers are tried before the built-in routing strategy
|
|
9
|
+
* 2. First plugin router that returns a non-null result wins
|
|
10
|
+
* 3. If all plugin routers return null, built-in strategy is used as fallback
|
|
11
|
+
* 4. Plugin routers receive the same story context as built-in routers
|
|
12
|
+
* 5. Router errors are caught and logged; fallback to next router in chain
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
16
|
+
import { DEFAULT_CONFIG } from "../../../src/config";
|
|
17
|
+
import * as loggerModule from "../../../src/logger";
|
|
18
|
+
import { PluginRegistry } from "../../../src/plugins/registry";
|
|
19
|
+
import type { NaxPlugin } from "../../../src/plugins/types";
|
|
20
|
+
import type { UserStory } from "../../../src/prd/types";
|
|
21
|
+
import { buildStrategyChain } from "../../../src/routing/builder";
|
|
22
|
+
import { routeStory } from "../../../src/routing/router";
|
|
23
|
+
import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../../../src/routing/strategy";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Test Helpers
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
function createTestStory(overrides?: Partial<UserStory>): UserStory {
|
|
30
|
+
return {
|
|
31
|
+
id: "US-TEST",
|
|
32
|
+
title: "Test story",
|
|
33
|
+
description: "Test description",
|
|
34
|
+
acceptanceCriteria: ["AC1", "AC2"],
|
|
35
|
+
tags: [],
|
|
36
|
+
dependencies: [],
|
|
37
|
+
status: "pending",
|
|
38
|
+
passes: false,
|
|
39
|
+
escalations: [],
|
|
40
|
+
attempts: 0,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createTestContext(overrides?: Partial<RoutingContext>): RoutingContext {
|
|
46
|
+
return {
|
|
47
|
+
config: DEFAULT_CONFIG,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createPluginRouter(name: string, routeFn: RoutingStrategy["route"]): RoutingStrategy {
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
route: routeFn,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createMockPlugin(pluginName: string, router?: RoutingStrategy): NaxPlugin {
|
|
60
|
+
const plugin: NaxPlugin = {
|
|
61
|
+
name: pluginName,
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
provides: router ? ["router"] : [],
|
|
64
|
+
extensions: {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (router) {
|
|
68
|
+
plugin.extensions.router = router;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return plugin;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
describe("Plugin router error handling", () => {
|
|
76
|
+
test("error in plugin router is caught and next router is tried", async () => {
|
|
77
|
+
const errorRouter = createPluginRouter("error-router", () => {
|
|
78
|
+
throw new Error("Plugin router error");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const successRouter = createPluginRouter("success-router", () => ({
|
|
82
|
+
complexity: "simple",
|
|
83
|
+
modelTier: "fast",
|
|
84
|
+
testStrategy: "test-after",
|
|
85
|
+
reasoning: "Success router decision",
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const registry = new PluginRegistry([
|
|
89
|
+
createMockPlugin("error-plugin", errorRouter),
|
|
90
|
+
createMockPlugin("success-plugin", successRouter),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const story = createTestStory();
|
|
94
|
+
const context = createTestContext();
|
|
95
|
+
|
|
96
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
97
|
+
|
|
98
|
+
// Second router should succeed
|
|
99
|
+
expect(decision.reasoning).toBe("Success router decision");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("error in plugin router is logged", async () => {
|
|
103
|
+
const loggedErrors: Array<{ category: string; message: string; data?: unknown }> = [];
|
|
104
|
+
|
|
105
|
+
// Mock logger to capture error logs
|
|
106
|
+
const mockLogger = {
|
|
107
|
+
error: (category: string, message: string, data?: unknown) => {
|
|
108
|
+
loggedErrors.push({ category, message, data });
|
|
109
|
+
},
|
|
110
|
+
info: () => {},
|
|
111
|
+
warn: () => {},
|
|
112
|
+
debug: () => {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
spyOn(loggerModule, "getSafeLogger").mockReturnValue(mockLogger as any);
|
|
116
|
+
|
|
117
|
+
const errorRouter = createPluginRouter("error-router", () => {
|
|
118
|
+
throw new Error("Plugin router failed");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const fallbackRouter = createPluginRouter("fallback-router", () => ({
|
|
122
|
+
complexity: "simple",
|
|
123
|
+
modelTier: "fast",
|
|
124
|
+
testStrategy: "test-after",
|
|
125
|
+
reasoning: "Fallback decision",
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
const registry = new PluginRegistry([
|
|
129
|
+
createMockPlugin("error-plugin", errorRouter),
|
|
130
|
+
createMockPlugin("fallback-plugin", fallbackRouter),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
const story = createTestStory();
|
|
134
|
+
const context = createTestContext();
|
|
135
|
+
|
|
136
|
+
await routeStory(story, context, "/tmp", registry);
|
|
137
|
+
|
|
138
|
+
// Verify error was logged
|
|
139
|
+
expect(loggedErrors.length).toBeGreaterThan(0);
|
|
140
|
+
const errorLog = loggedErrors.find(
|
|
141
|
+
(log) => log.message.includes("error-router") || log.message.includes("Plugin router failed"),
|
|
142
|
+
);
|
|
143
|
+
expect(errorLog).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("multiple router errors are caught and keyword fallback succeeds", async () => {
|
|
147
|
+
const errorRouter1 = createPluginRouter("error-router-1", () => {
|
|
148
|
+
throw new Error("Router 1 error");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const errorRouter2 = createPluginRouter("error-router-2", () => {
|
|
152
|
+
throw new Error("Router 2 error");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const registry = new PluginRegistry([
|
|
156
|
+
createMockPlugin("error-plugin-1", errorRouter1),
|
|
157
|
+
createMockPlugin("error-plugin-2", errorRouter2),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
const story = createTestStory({
|
|
161
|
+
title: "Simple task",
|
|
162
|
+
description: "Simple description",
|
|
163
|
+
acceptanceCriteria: ["AC1"],
|
|
164
|
+
tags: [],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const context = createTestContext();
|
|
168
|
+
|
|
169
|
+
// Should not throw; keyword strategy should succeed
|
|
170
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
171
|
+
|
|
172
|
+
expect(decision.complexity).toBe("simple");
|
|
173
|
+
expect(decision.modelTier).toBe("fast");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("async error in plugin router is caught", async () => {
|
|
177
|
+
const asyncErrorRouter = createPluginRouter("async-error-router", async () => {
|
|
178
|
+
await Bun.sleep(10);
|
|
179
|
+
throw new Error("Async plugin error");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const successRouter = createPluginRouter("success-router", () => ({
|
|
183
|
+
complexity: "medium",
|
|
184
|
+
modelTier: "balanced",
|
|
185
|
+
testStrategy: "test-after",
|
|
186
|
+
reasoning: "Success after async error",
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
const registry = new PluginRegistry([
|
|
190
|
+
createMockPlugin("async-error-plugin", asyncErrorRouter),
|
|
191
|
+
createMockPlugin("success-plugin", successRouter),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const story = createTestStory();
|
|
195
|
+
const context = createTestContext();
|
|
196
|
+
|
|
197
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
198
|
+
|
|
199
|
+
// Should succeed with second router
|
|
200
|
+
expect(decision.reasoning).toBe("Success after async error");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("error in last plugin router falls back to keyword strategy", async () => {
|
|
204
|
+
const errorRouter = createPluginRouter("error-router", () => {
|
|
205
|
+
throw new Error("Last plugin router error");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const registry = new PluginRegistry([createMockPlugin("error-plugin", errorRouter)]);
|
|
209
|
+
|
|
210
|
+
const story = createTestStory({
|
|
211
|
+
title: "Fix typo",
|
|
212
|
+
description: "Fix typo in README",
|
|
213
|
+
acceptanceCriteria: ["Typo fixed"],
|
|
214
|
+
tags: [],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const context = createTestContext();
|
|
218
|
+
|
|
219
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
220
|
+
|
|
221
|
+
// Keyword strategy should succeed
|
|
222
|
+
expect(decision.complexity).toBe("simple");
|
|
223
|
+
expect(decision.modelTier).toBe("fast");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("error message includes plugin name for debugging", async () => {
|
|
227
|
+
const loggedErrors: Array<{ category: string; message: string; data?: unknown }> = [];
|
|
228
|
+
|
|
229
|
+
const mockLogger = {
|
|
230
|
+
error: (category: string, message: string, data?: unknown) => {
|
|
231
|
+
loggedErrors.push({ category, message, data });
|
|
232
|
+
},
|
|
233
|
+
info: () => {},
|
|
234
|
+
warn: () => {},
|
|
235
|
+
debug: () => {},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
spyOn(loggerModule, "getSafeLogger").mockReturnValue(mockLogger as any);
|
|
239
|
+
|
|
240
|
+
const errorRouter = createPluginRouter("my-custom-router", () => {
|
|
241
|
+
throw new Error("Custom error");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const successRouter = createPluginRouter("success-router", () => ({
|
|
245
|
+
complexity: "simple",
|
|
246
|
+
modelTier: "fast",
|
|
247
|
+
testStrategy: "test-after",
|
|
248
|
+
reasoning: "Success",
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
const registry = new PluginRegistry([
|
|
252
|
+
createMockPlugin("custom-plugin", errorRouter),
|
|
253
|
+
createMockPlugin("success-plugin", successRouter),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const story = createTestStory();
|
|
257
|
+
const context = createTestContext();
|
|
258
|
+
|
|
259
|
+
await routeStory(story, context, "/tmp", registry);
|
|
260
|
+
|
|
261
|
+
// Verify error log includes plugin router name
|
|
262
|
+
const errorLog = loggedErrors.find(
|
|
263
|
+
(log) => log.message.includes("my-custom-router") || log.data?.toString().includes("my-custom-router"),
|
|
264
|
+
);
|
|
265
|
+
expect(errorLog).toBeDefined();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Integration Tests: Real-world scenarios
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
describe("Plugin routing integration scenarios", () => {
|
|
274
|
+
test("premium plugin forces security stories to expert tier", async () => {
|
|
275
|
+
const premiumRouter = createPluginRouter("premium-security-router", (story, context) => {
|
|
276
|
+
if (story.tags.includes("security") || story.tags.includes("auth")) {
|
|
277
|
+
return {
|
|
278
|
+
complexity: "expert",
|
|
279
|
+
modelTier: "powerful",
|
|
280
|
+
testStrategy: "three-session-tdd",
|
|
281
|
+
reasoning: "Premium plugin: security/auth always use expert tier",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const plugin = createMockPlugin("premium-plugin", premiumRouter);
|
|
288
|
+
const registry = new PluginRegistry([plugin]);
|
|
289
|
+
|
|
290
|
+
// Simple story with security tag
|
|
291
|
+
const story = createTestStory({
|
|
292
|
+
title: "Update login button text",
|
|
293
|
+
description: "Change 'Login' to 'Sign In'",
|
|
294
|
+
acceptanceCriteria: ["Button text updated"],
|
|
295
|
+
tags: ["security", "ui"],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const context = createTestContext();
|
|
299
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
300
|
+
|
|
301
|
+
// Plugin should force expert tier despite simple nature
|
|
302
|
+
expect(decision.complexity).toBe("expert");
|
|
303
|
+
expect(decision.modelTier).toBe("powerful");
|
|
304
|
+
expect(decision.reasoning).toContain("Premium plugin");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("cost-optimization plugin downgrades simple docs to fast tier", async () => {
|
|
308
|
+
const costOptimizationRouter = createPluginRouter("cost-optimization-router", (story, context) => {
|
|
309
|
+
if (story.tags.includes("docs") && story.acceptanceCriteria.length <= 2) {
|
|
310
|
+
return {
|
|
311
|
+
complexity: "simple",
|
|
312
|
+
modelTier: "fast",
|
|
313
|
+
testStrategy: "test-after",
|
|
314
|
+
reasoning: "Cost optimization: simple docs use fast tier",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const plugin = createMockPlugin("cost-optimization-plugin", costOptimizationRouter);
|
|
321
|
+
const registry = new PluginRegistry([plugin]);
|
|
322
|
+
|
|
323
|
+
const story = createTestStory({
|
|
324
|
+
title: "Update API documentation",
|
|
325
|
+
description: "Add examples to API docs",
|
|
326
|
+
acceptanceCriteria: ["Examples added"],
|
|
327
|
+
tags: ["docs"],
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const context = createTestContext();
|
|
331
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
332
|
+
|
|
333
|
+
expect(decision.modelTier).toBe("fast");
|
|
334
|
+
expect(decision.reasoning).toContain("Cost optimization");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("domain-specific plugin routes database migrations to expert tier", async () => {
|
|
338
|
+
const domainRouter = createPluginRouter("domain-router", (story, context) => {
|
|
339
|
+
const text = [story.title, story.description, ...story.tags].join(" ").toLowerCase();
|
|
340
|
+
if (text.includes("migration") || text.includes("database") || text.includes("schema")) {
|
|
341
|
+
return {
|
|
342
|
+
complexity: "expert",
|
|
343
|
+
modelTier: "powerful",
|
|
344
|
+
testStrategy: "three-session-tdd",
|
|
345
|
+
reasoning: "Domain-specific: database changes require expert review",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const plugin = createMockPlugin("domain-plugin", domainRouter);
|
|
352
|
+
const registry = new PluginRegistry([plugin]);
|
|
353
|
+
|
|
354
|
+
const story = createTestStory({
|
|
355
|
+
title: "Add user_email column",
|
|
356
|
+
description: "Add email column to users table migration",
|
|
357
|
+
acceptanceCriteria: ["Column added", "Migration tested"],
|
|
358
|
+
tags: ["database"],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const context = createTestContext();
|
|
362
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
363
|
+
|
|
364
|
+
expect(decision.complexity).toBe("expert");
|
|
365
|
+
expect(decision.reasoning).toContain("Domain-specific");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("multiple plugins: first matching plugin wins", async () => {
|
|
369
|
+
const securityRouter = createPluginRouter("security-router", (story) => {
|
|
370
|
+
if (story.tags.includes("security")) {
|
|
371
|
+
return {
|
|
372
|
+
complexity: "expert",
|
|
373
|
+
modelTier: "powerful",
|
|
374
|
+
testStrategy: "three-session-tdd",
|
|
375
|
+
reasoning: "Security plugin decision",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const uiRouter = createPluginRouter("ui-router", (story) => {
|
|
382
|
+
if (story.tags.includes("ui")) {
|
|
383
|
+
return {
|
|
384
|
+
complexity: "simple",
|
|
385
|
+
modelTier: "fast",
|
|
386
|
+
testStrategy: "three-session-tdd-lite",
|
|
387
|
+
reasoning: "UI plugin decision",
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const registry = new PluginRegistry([
|
|
394
|
+
createMockPlugin("security-plugin", securityRouter),
|
|
395
|
+
createMockPlugin("ui-plugin", uiRouter),
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
// Story with both tags
|
|
399
|
+
const story = createTestStory({
|
|
400
|
+
title: "Update security settings UI",
|
|
401
|
+
description: "Redesign security settings page",
|
|
402
|
+
acceptanceCriteria: ["UI updated", "Settings work"],
|
|
403
|
+
tags: ["security", "ui"],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const context = createTestContext();
|
|
407
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
408
|
+
|
|
409
|
+
// Security plugin is first, so it should win
|
|
410
|
+
expect(decision.reasoning).toBe("Security plugin decision");
|
|
411
|
+
expect(decision.complexity).toBe("expert");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("plugin router can delegate based on conditional logic", async () => {
|
|
415
|
+
const conditionalRouter = createPluginRouter("conditional-router", (story, context) => {
|
|
416
|
+
// Only handle stories with "critical" tag
|
|
417
|
+
if (story.tags.includes("critical")) {
|
|
418
|
+
return {
|
|
419
|
+
complexity: "expert",
|
|
420
|
+
modelTier: "powerful",
|
|
421
|
+
testStrategy: "three-session-tdd",
|
|
422
|
+
reasoning: "Critical tag forces expert tier",
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Delegate all other stories to built-in strategy
|
|
426
|
+
return null;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const plugin = createMockPlugin("conditional-plugin", conditionalRouter);
|
|
430
|
+
const registry = new PluginRegistry([plugin]);
|
|
431
|
+
|
|
432
|
+
// Non-critical story
|
|
433
|
+
const normalStory = createTestStory({
|
|
434
|
+
title: "Add button",
|
|
435
|
+
description: "Add submit button",
|
|
436
|
+
acceptanceCriteria: ["Button added"],
|
|
437
|
+
tags: ["ui"],
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const context = createTestContext();
|
|
441
|
+
const normalDecision = await routeStory(normalStory, context, "/tmp", registry);
|
|
442
|
+
|
|
443
|
+
// Should fall back to keyword strategy
|
|
444
|
+
expect(normalDecision.complexity).toBe("simple");
|
|
445
|
+
expect(normalDecision.modelTier).toBe("fast");
|
|
446
|
+
|
|
447
|
+
// Critical story
|
|
448
|
+
const criticalStory = createTestStory({
|
|
449
|
+
title: "Add button",
|
|
450
|
+
description: "Add submit button",
|
|
451
|
+
acceptanceCriteria: ["Button added"],
|
|
452
|
+
tags: ["ui", "critical"],
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const criticalDecision = await routeStory(criticalStory, context, "/tmp", registry);
|
|
456
|
+
|
|
457
|
+
// Plugin should handle it
|
|
458
|
+
expect(criticalDecision.complexity).toBe("expert");
|
|
459
|
+
expect(criticalDecision.reasoning).toContain("Critical tag");
|
|
460
|
+
});
|
|
461
|
+
});
|