@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-2
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/build/index.js +2 -1
- package/build/prompts/code-reuse.d.ts +7 -1
- package/build/prompts/code-reuse.js +8 -5
- package/build/prompts/code-reuse.test.d.ts +1 -0
- package/build/prompts/code-reuse.test.js +62 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
- package/build/prompts/pom-aware-code-reuse.js +100 -53
- package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
- package/build/prompts/pom-aware-code-reuse.test.js +11 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
- package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
- package/build/prompts/test-recommendation/recommendationSections.js +5 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
- package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
- package/build/prompts/testbot/testbot-prompts.js +67 -21
- package/build/prompts/testbot/testbot-prompts.test.js +44 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
- package/build/recommendation/budgeters/shared.d.ts +19 -0
- package/build/recommendation/budgeters/shared.js +66 -0
- package/build/recommendation/discriminators.d.ts +31 -0
- package/build/recommendation/discriminators.js +355 -0
- package/build/recommendation/discriminators.test.d.ts +1 -0
- package/build/recommendation/discriminators.test.js +324 -0
- package/build/recommendation/diversity.d.ts +47 -0
- package/build/recommendation/diversity.js +101 -0
- package/build/recommendation/diversity.test.d.ts +1 -0
- package/build/recommendation/diversity.test.js +77 -0
- package/build/recommendation/planRanker.d.ts +50 -0
- package/build/recommendation/planRanker.js +67 -0
- package/build/recommendation/planRanker.test.d.ts +1 -0
- package/build/recommendation/planRanker.test.js +110 -0
- package/build/recommendation/testFixtures.d.ts +25 -0
- package/build/recommendation/testFixtures.js +45 -0
- package/build/resources/testbotResource.js +4 -1
- package/build/services/ScenarioGenerationService.d.ts +5 -0
- package/build/services/ScenarioGenerationService.js +16 -1
- package/build/services/ScenarioGenerationService.test.js +44 -0
- package/build/services/TestExecutionService.d.ts +15 -1
- package/build/services/TestExecutionService.js +210 -55
- package/build/services/TestExecutionService.test.js +397 -0
- package/build/services/TestGenerationService.js +19 -1
- package/build/services/TestGenerationService.test.js +58 -0
- package/build/tool-phases.js +1 -0
- package/build/toolNames.d.ts +19 -0
- package/build/toolNames.js +19 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
- package/build/tools/code-refactor/codeReuseTool.js +130 -4
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
- package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
- package/build/tools/executeSkyrampTestTool.js +8 -2
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
- package/build/tools/generate-tests/generateContractRestTool.js +11 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
- package/build/tools/generate-tests/planGuard.d.ts +13 -0
- package/build/tools/generate-tests/planGuard.js +78 -0
- package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
- package/build/tools/generate-tests/planGuard.test.js +185 -0
- package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
- package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
- package/build/tools/generate-tests/scenarioLint.js +150 -0
- package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
- package/build/tools/generate-tests/scenarioLint.test.js +100 -0
- package/build/tools/submitReportTool.js +78 -0
- package/build/tools/submitReportTool.test.js +255 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
- package/build/tools/test-management/registerTestPlanTool.js +329 -0
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
- package/build/types/Recommendation.d.ts +97 -0
- package/build/types/Recommendation.js +48 -0
- package/build/types/RepositoryAnalysis.d.ts +14 -14
- package/build/types/TestExecution.d.ts +2 -0
- package/build/types/TestRecommendation.d.ts +12 -1
- package/build/types/TestRecommendation.js +26 -11
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +47 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/planMatchKeys.d.ts +61 -0
- package/build/utils/planMatchKeys.js +125 -0
- package/build/utils/pom-scope/import-expansion.d.ts +5 -0
- package/build/utils/pom-scope/import-expansion.js +32 -0
- package/build/utils/pom-scope/index.d.ts +39 -0
- package/build/utils/pom-scope/index.js +120 -0
- package/build/utils/pom-scope/index.test.d.ts +1 -0
- package/build/utils/pom-scope/index.test.js +239 -0
- package/build/utils/pom-scope/pom-files.d.ts +3 -0
- package/build/utils/pom-scope/pom-files.js +48 -0
- package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
- package/build/utils/pom-scope/pom-files.test.js +29 -0
- package/build/utils/pom-scope/scoring.d.ts +20 -0
- package/build/utils/pom-scope/scoring.js +45 -0
- package/build/utils/pom-scope/scoring.test.d.ts +1 -0
- package/build/utils/pom-scope/scoring.test.js +39 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
- package/build/utils/pom-scope/selector-extractor.js +57 -0
- package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
- package/build/utils/pom-scope/selector-extractor.test.js +67 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
- package/build/utils/pom-verify/bindings.d.ts +19 -0
- package/build/utils/pom-verify/bindings.js +161 -0
- package/build/utils/pom-verify/bindings.test.d.ts +1 -0
- package/build/utils/pom-verify/bindings.test.js +164 -0
- package/build/utils/pom-verify/calls.d.ts +16 -0
- package/build/utils/pom-verify/calls.js +42 -0
- package/build/utils/pom-verify/calls.test.d.ts +1 -0
- package/build/utils/pom-verify/calls.test.js +61 -0
- package/build/utils/pom-verify/index.d.ts +4 -0
- package/build/utils/pom-verify/index.js +4 -0
- package/build/utils/pom-verify/resolve.d.ts +7 -0
- package/build/utils/pom-verify/resolve.js +27 -0
- package/build/utils/pom-verify/resolve.test.d.ts +1 -0
- package/build/utils/pom-verify/resolve.test.js +68 -0
- package/build/utils/pom-verify/strip.d.ts +9 -0
- package/build/utils/pom-verify/strip.js +89 -0
- package/build/utils/pom-verify/verify.d.ts +14 -0
- package/build/utils/pom-verify/verify.js +158 -0
- package/build/utils/pom-verify/verify.test.d.ts +1 -0
- package/build/utils/pom-verify/verify.test.js +325 -0
- package/build/utils/reportVerification.d.ts +61 -0
- package/build/utils/reportVerification.js +104 -0
- package/build/utils/reportVerification.test.d.ts +1 -0
- package/build/utils/reportVerification.test.js +185 -0
- package/build/utils/scenarioDrafting.js +5 -5
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +9 -1
- package/build/utils/workspaceAuth.js +25 -5
- package/build/utils/workspaceAuth.test.js +48 -0
- package/build/workspace/workspace.d.ts +20 -0
- package/build/workspace/workspace.js +4 -0
- package/build/workspace/workspace.test.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DraftedScenario } from "../types/RepositoryAnalysis.js";
|
|
2
|
+
import { PriorityTier } from "../types/TestRecommendation.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reorder a rank-ordered list so each attack-surface security_boundary scenario
|
|
5
|
+
* sits immediately before the first ordinary direct-auth boundary — keeping
|
|
6
|
+
* destructive sibling auth tests bundled ahead of their read counterparts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function prioritizeAttackSurfaceBundles<T extends {
|
|
9
|
+
scenario: DraftedScenario;
|
|
10
|
+
}>(items: T[]): T[];
|
|
11
|
+
/** The test-type inference used everywhere GENERATE slots are distributed:
|
|
12
|
+
* explicit testType, else single-step ⇒ contract, multi-step ⇒ integration
|
|
13
|
+
* (the same inference used when rendering plan items). */
|
|
14
|
+
export declare function inferScenarioType(s: DraftedScenario): string;
|
|
15
|
+
/** Protected items always take a GENERATE slot before any round-robin:
|
|
16
|
+
* CRITICAL-priority and attack-surface security_boundary scenarios. */
|
|
17
|
+
export declare function isProtectedCandidate(priority: PriorityTier, scenario: DraftedScenario): boolean;
|
|
18
|
+
/** Bucket items by type, preserving rank order within each bucket and
|
|
19
|
+
* first-appearance order across buckets (so the highest-ranked type wins
|
|
20
|
+
* round 1 of any subsequent round-robin). */
|
|
21
|
+
export declare function bucketByType<T>(items: T[], typeOf: (item: T) => string): {
|
|
22
|
+
order: string[];
|
|
23
|
+
buckets: Map<string, T[]>;
|
|
24
|
+
};
|
|
25
|
+
/** Round-robin one item per non-empty bucket per round (skipping exhausted
|
|
26
|
+
* buckets, so their freed slots spill over to the next type) until
|
|
27
|
+
* `selected` reaches `count` or every bucket is empty. Mutates `selected`
|
|
28
|
+
* and the buckets. */
|
|
29
|
+
export declare function roundRobinFill<T>(selected: T[], order: string[], buckets: Map<string, T[]>, count: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Select `count` items from a rank-ordered list, distributing GENERATE slots
|
|
32
|
+
* EVENLY across the test types present, with spillover.
|
|
33
|
+
*
|
|
34
|
+
* Policy (kept identical to the multi-repo prose in testbot-prompts.ts's
|
|
35
|
+
* "Cross-repo test generation" block — change both together):
|
|
36
|
+
* - Protected items first (see isProtectedCandidate) — they must stay in GENERATE.
|
|
37
|
+
* - Bucket the rest by inferred test type and round-robin (see bucketByType /
|
|
38
|
+
* roundRobinFill), preserving rank order within each bucket.
|
|
39
|
+
*
|
|
40
|
+
* Degenerate cases match the previous pure rank-order slice exactly: a single type
|
|
41
|
+
* present, or `count >= items.length`, returns the same items in the same order —
|
|
42
|
+
* so backend-only / single-type runs are unchanged (no regression).
|
|
43
|
+
*/
|
|
44
|
+
export declare function roundRobinByType<T extends {
|
|
45
|
+
scenario: DraftedScenario;
|
|
46
|
+
priority: PriorityTier;
|
|
47
|
+
}>(rankOrdered: T[], count: number): T[];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { PriorityTier } from "../types/TestRecommendation.js";
|
|
2
|
+
import { isAttackSurfaceSecurityBoundary, isOrdinaryDirectAuthBoundary, } from "../prompts/test-recommendation/recommendationShared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reorder a rank-ordered list so each attack-surface security_boundary scenario
|
|
5
|
+
* sits immediately before the first ordinary direct-auth boundary — keeping
|
|
6
|
+
* destructive sibling auth tests bundled ahead of their read counterparts.
|
|
7
|
+
*/
|
|
8
|
+
export function prioritizeAttackSurfaceBundles(items) {
|
|
9
|
+
const reordered = [];
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
if (isAttackSurfaceSecurityBoundary(item.scenario)) {
|
|
12
|
+
const firstDirectAuthIndex = reordered.findIndex((candidate) => isOrdinaryDirectAuthBoundary(candidate.scenario));
|
|
13
|
+
if (firstDirectAuthIndex >= 0) {
|
|
14
|
+
reordered.splice(firstDirectAuthIndex, 0, item);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
reordered.push(item);
|
|
19
|
+
}
|
|
20
|
+
return reordered;
|
|
21
|
+
}
|
|
22
|
+
/** The test-type inference used everywhere GENERATE slots are distributed:
|
|
23
|
+
* explicit testType, else single-step ⇒ contract, multi-step ⇒ integration
|
|
24
|
+
* (the same inference used when rendering plan items). */
|
|
25
|
+
export function inferScenarioType(s) {
|
|
26
|
+
return s.testType ?? (s.steps.length === 1 ? "contract" : "integration");
|
|
27
|
+
}
|
|
28
|
+
/** Protected items always take a GENERATE slot before any round-robin:
|
|
29
|
+
* CRITICAL-priority and attack-surface security_boundary scenarios. */
|
|
30
|
+
export function isProtectedCandidate(priority, scenario) {
|
|
31
|
+
return (priority === PriorityTier.CRITICAL ||
|
|
32
|
+
isAttackSurfaceSecurityBoundary(scenario));
|
|
33
|
+
}
|
|
34
|
+
/** Bucket items by type, preserving rank order within each bucket and
|
|
35
|
+
* first-appearance order across buckets (so the highest-ranked type wins
|
|
36
|
+
* round 1 of any subsequent round-robin). */
|
|
37
|
+
export function bucketByType(items, typeOf) {
|
|
38
|
+
const order = [];
|
|
39
|
+
const buckets = new Map();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const t = typeOf(item);
|
|
42
|
+
if (!buckets.has(t)) {
|
|
43
|
+
buckets.set(t, []);
|
|
44
|
+
order.push(t);
|
|
45
|
+
}
|
|
46
|
+
buckets.get(t).push(item);
|
|
47
|
+
}
|
|
48
|
+
return { order, buckets };
|
|
49
|
+
}
|
|
50
|
+
/** Round-robin one item per non-empty bucket per round (skipping exhausted
|
|
51
|
+
* buckets, so their freed slots spill over to the next type) until
|
|
52
|
+
* `selected` reaches `count` or every bucket is empty. Mutates `selected`
|
|
53
|
+
* and the buckets. */
|
|
54
|
+
export function roundRobinFill(selected, order, buckets, count) {
|
|
55
|
+
while (selected.length < count &&
|
|
56
|
+
order.some((t) => (buckets.get(t)?.length ?? 0) > 0)) {
|
|
57
|
+
for (const t of order) {
|
|
58
|
+
if (selected.length >= count)
|
|
59
|
+
break;
|
|
60
|
+
const bucket = buckets.get(t);
|
|
61
|
+
if (bucket && bucket.length > 0)
|
|
62
|
+
selected.push(bucket.shift());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Select `count` items from a rank-ordered list, distributing GENERATE slots
|
|
68
|
+
* EVENLY across the test types present, with spillover.
|
|
69
|
+
*
|
|
70
|
+
* Policy (kept identical to the multi-repo prose in testbot-prompts.ts's
|
|
71
|
+
* "Cross-repo test generation" block — change both together):
|
|
72
|
+
* - Protected items first (see isProtectedCandidate) — they must stay in GENERATE.
|
|
73
|
+
* - Bucket the rest by inferred test type and round-robin (see bucketByType /
|
|
74
|
+
* roundRobinFill), preserving rank order within each bucket.
|
|
75
|
+
*
|
|
76
|
+
* Degenerate cases match the previous pure rank-order slice exactly: a single type
|
|
77
|
+
* present, or `count >= items.length`, returns the same items in the same order —
|
|
78
|
+
* so backend-only / single-type runs are unchanged (no regression).
|
|
79
|
+
*/
|
|
80
|
+
export function roundRobinByType(rankOrdered, count) {
|
|
81
|
+
if (count <= 0)
|
|
82
|
+
return [];
|
|
83
|
+
// Everything fits → no need to bucket; identical to the old slice.
|
|
84
|
+
if (count >= rankOrdered.length)
|
|
85
|
+
return rankOrdered.slice(0, count);
|
|
86
|
+
// Protected items occupy GENERATE slots first, in rank order.
|
|
87
|
+
const selected = [];
|
|
88
|
+
const remaining = [];
|
|
89
|
+
for (const item of rankOrdered) {
|
|
90
|
+
if (selected.length < count &&
|
|
91
|
+
isProtectedCandidate(item.priority, item.scenario)) {
|
|
92
|
+
selected.push(item);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
remaining.push(item);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const { order, buckets } = bucketByType(remaining, (item) => inferScenarioType(item.scenario));
|
|
99
|
+
roundRobinFill(selected, order, buckets, count);
|
|
100
|
+
return selected;
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { roundRobinByType, prioritizeAttackSurfaceBundles } from "./diversity.js";
|
|
2
|
+
import { PriorityTier } from "../types/TestRecommendation.js";
|
|
3
|
+
// Minimal DraftedScenario factory — only the fields the diversity helpers read
|
|
4
|
+
// (category, description, steps.length, testType) matter here.
|
|
5
|
+
function scen(name, opts = {}) {
|
|
6
|
+
return {
|
|
7
|
+
scenarioName: name,
|
|
8
|
+
description: opts.description ?? name,
|
|
9
|
+
category: opts.category ?? "workflow",
|
|
10
|
+
priority: "high",
|
|
11
|
+
steps: Array.from({ length: opts.steps ?? 2 }, () => ({})),
|
|
12
|
+
chainingKeys: [],
|
|
13
|
+
requiresAuth: false,
|
|
14
|
+
estimatedComplexity: "moderate",
|
|
15
|
+
testType: opts.testType,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function cand(name, priority, opts = {}) {
|
|
19
|
+
return { scenario: scen(name, opts), priority };
|
|
20
|
+
}
|
|
21
|
+
const names = (items) => items.map((i) => i.scenario.scenarioName);
|
|
22
|
+
describe("roundRobinByType", () => {
|
|
23
|
+
it("returns [] when count <= 0", () => {
|
|
24
|
+
const items = [cand("a", PriorityTier.HIGH), cand("b", PriorityTier.HIGH)];
|
|
25
|
+
expect(roundRobinByType(items, 0)).toEqual([]);
|
|
26
|
+
expect(roundRobinByType(items, -1)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it("returns the full list in rank order when count >= length (degenerate == slice)", () => {
|
|
29
|
+
const items = [cand("a", PriorityTier.HIGH), cand("b", PriorityTier.HIGH), cand("c", PriorityTier.HIGH)];
|
|
30
|
+
expect(names(roundRobinByType(items, 3))).toEqual(["a", "b", "c"]);
|
|
31
|
+
expect(names(roundRobinByType(items, 5))).toEqual(["a", "b", "c"]);
|
|
32
|
+
});
|
|
33
|
+
it("with a single test type present, returns the first `count` by rank", () => {
|
|
34
|
+
const items = [
|
|
35
|
+
cand("i1", PriorityTier.HIGH, { testType: "integration" }),
|
|
36
|
+
cand("i2", PriorityTier.HIGH, { testType: "integration" }),
|
|
37
|
+
cand("i3", PriorityTier.HIGH, { testType: "integration" }),
|
|
38
|
+
];
|
|
39
|
+
expect(names(roundRobinByType(items, 2))).toEqual(["i1", "i2"]);
|
|
40
|
+
});
|
|
41
|
+
it("distributes one slot per present test type before a second of any type", () => {
|
|
42
|
+
const items = [
|
|
43
|
+
cand("i1", PriorityTier.HIGH, { testType: "integration" }),
|
|
44
|
+
cand("i2", PriorityTier.HIGH, { testType: "integration" }),
|
|
45
|
+
cand("c1", PriorityTier.HIGH, { testType: "contract" }),
|
|
46
|
+
cand("i3", PriorityTier.HIGH, { testType: "integration" }),
|
|
47
|
+
];
|
|
48
|
+
// First-appearance bucket order is [integration, contract]; round 1 picks one of each.
|
|
49
|
+
expect(names(roundRobinByType(items, 2))).toEqual(["i1", "c1"]);
|
|
50
|
+
});
|
|
51
|
+
it("selects protected (CRITICAL) items first, regardless of type or rank position", () => {
|
|
52
|
+
const items = [
|
|
53
|
+
cand("i1", PriorityTier.HIGH, { testType: "integration" }),
|
|
54
|
+
cand("i2", PriorityTier.HIGH, { testType: "integration" }),
|
|
55
|
+
cand("c1", PriorityTier.CRITICAL, { testType: "contract" }),
|
|
56
|
+
];
|
|
57
|
+
const result = names(roundRobinByType(items, 2));
|
|
58
|
+
expect(result[0]).toBe("c1");
|
|
59
|
+
expect(result).toContain("i1");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("prioritizeAttackSurfaceBundles", () => {
|
|
63
|
+
it("moves an attack-surface security_boundary item ahead of the first ordinary direct-auth boundary", () => {
|
|
64
|
+
const items = [
|
|
65
|
+
cand("ordinary", PriorityTier.HIGH, { category: "security_boundary", description: "Auth boundary: GET /x" }),
|
|
66
|
+
cand("attack", PriorityTier.HIGH, {
|
|
67
|
+
category: "security_boundary",
|
|
68
|
+
description: "Attack-surface auth boundary: DELETE /x",
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
expect(names(prioritizeAttackSurfaceBundles(items))).toEqual(["attack", "ordinary"]);
|
|
72
|
+
});
|
|
73
|
+
it("leaves ordering unchanged when there is no attack-surface item", () => {
|
|
74
|
+
const items = [cand("a", PriorityTier.HIGH), cand("b", PriorityTier.HIGH)];
|
|
75
|
+
expect(names(prioritizeAttackSurfaceBundles(items))).toEqual(["a", "b"]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Candidate, BudgetContext, Demotion, SelectionResult } from "../types/Recommendation.js";
|
|
2
|
+
import { ScenarioCategory } from "../types/TestRecommendation.js";
|
|
3
|
+
/** Options for {@link rankCandidates}. Reserved for phase-2 tuning. */
|
|
4
|
+
export interface RankOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Categories that take the top carve-out tier ahead of everything else.
|
|
7
|
+
* Defaults to the `CATEGORY_PRIORITY === "CRITICAL"` categories (new_endpoint,
|
|
8
|
+
* bug_caught). Exposed so phase 2 can tune the carve-out WITHOUT reintroducing
|
|
9
|
+
* the agent's priority tag as a ranking input.
|
|
10
|
+
*/
|
|
11
|
+
carveOutCategories?: ScenarioCategory[];
|
|
12
|
+
}
|
|
13
|
+
/** Context for {@link selectPlan}: the budget context plus the demotion channel
|
|
14
|
+
* the register-plan tool fills from discriminator verification. */
|
|
15
|
+
export interface SelectPlanContext extends BudgetContext {
|
|
16
|
+
/** Demoted claims (failed discriminator verification) to surface on the result. */
|
|
17
|
+
demotions?: Demotion[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Rank test candidates for the register-plan checkpoint. Pure and fully
|
|
21
|
+
* deterministic: the same set of candidates always yields the same order,
|
|
22
|
+
* independent of input order (the final tiebreak is the stable `candidateId`).
|
|
23
|
+
*
|
|
24
|
+
* Ordering (highest first):
|
|
25
|
+
* 1. Carve-out — bug_caught / CRITICAL-category scenarios (new_endpoint,
|
|
26
|
+
* bug_caught), preserving the protected-first convention of
|
|
27
|
+
* `roundRobinByType` / `prioritizeAttackSurfaceBundles`.
|
|
28
|
+
* 2. Verified discriminators — candidates whose declared discriminator survived
|
|
29
|
+
* `validateDiscriminator` (marked via `verifiedDiscriminator`) float ahead of
|
|
30
|
+
* unverified peers in the same tier.
|
|
31
|
+
* 3. `CATEGORY_PRIORITY` mapped over `scenario.category` (CRITICAL > HIGH >
|
|
32
|
+
* MEDIUM > LOW).
|
|
33
|
+
* 4. Stable tiebreak on `candidateId` for determinism.
|
|
34
|
+
*
|
|
35
|
+
* IMPORTANT: the agent-supplied `priority` field (`scenario.priority`) is
|
|
36
|
+
* DELIBERATELY IGNORED here. Round-1 experiments found it systematically
|
|
37
|
+
* miscalibrated — the agent labels genuinely discriminating tests "low" and
|
|
38
|
+
* generic happy-path tests "high" — so ranking on it inverts the intended order.
|
|
39
|
+
* Priority in this pipeline is derived from the scenario's category and from the
|
|
40
|
+
* verified discriminator, never from the agent's self-assessment.
|
|
41
|
+
*/
|
|
42
|
+
export declare function rankCandidates(candidates: Candidate[], opts?: RankOptions): Candidate[];
|
|
43
|
+
/**
|
|
44
|
+
* Convenience composition: rank the candidates, split GENERATE vs ADDITIONAL via
|
|
45
|
+
* the diversity-balanced budgeter, and surface the caller-supplied demotions on
|
|
46
|
+
* the result. The caller (the register-plan tool) runs `validateDiscriminator`
|
|
47
|
+
* first — setting `verifiedDiscriminator` on the candidates that passed and
|
|
48
|
+
* passing the failed claims through `ctx.demotions`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function selectPlan(candidates: Candidate[], ctx: SelectPlanContext, opts?: RankOptions): SelectionResult;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { CATEGORY_PRIORITY, PriorityTier } from "../types/TestRecommendation.js";
|
|
2
|
+
import { diversityBalancedBudgeter } from "./budgeters/diversityBalancedBudgeter.js";
|
|
3
|
+
const PRIORITY_RANK = {
|
|
4
|
+
CRITICAL: 0,
|
|
5
|
+
HIGH: 1,
|
|
6
|
+
MEDIUM: 2,
|
|
7
|
+
LOW: 3,
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_CARVE_OUT_CATEGORIES = Object.keys(CATEGORY_PRIORITY).filter((category) => CATEGORY_PRIORITY[category] === PriorityTier.CRITICAL);
|
|
10
|
+
/**
|
|
11
|
+
* Rank test candidates for the register-plan checkpoint. Pure and fully
|
|
12
|
+
* deterministic: the same set of candidates always yields the same order,
|
|
13
|
+
* independent of input order (the final tiebreak is the stable `candidateId`).
|
|
14
|
+
*
|
|
15
|
+
* Ordering (highest first):
|
|
16
|
+
* 1. Carve-out — bug_caught / CRITICAL-category scenarios (new_endpoint,
|
|
17
|
+
* bug_caught), preserving the protected-first convention of
|
|
18
|
+
* `roundRobinByType` / `prioritizeAttackSurfaceBundles`.
|
|
19
|
+
* 2. Verified discriminators — candidates whose declared discriminator survived
|
|
20
|
+
* `validateDiscriminator` (marked via `verifiedDiscriminator`) float ahead of
|
|
21
|
+
* unverified peers in the same tier.
|
|
22
|
+
* 3. `CATEGORY_PRIORITY` mapped over `scenario.category` (CRITICAL > HIGH >
|
|
23
|
+
* MEDIUM > LOW).
|
|
24
|
+
* 4. Stable tiebreak on `candidateId` for determinism.
|
|
25
|
+
*
|
|
26
|
+
* IMPORTANT: the agent-supplied `priority` field (`scenario.priority`) is
|
|
27
|
+
* DELIBERATELY IGNORED here. Round-1 experiments found it systematically
|
|
28
|
+
* miscalibrated — the agent labels genuinely discriminating tests "low" and
|
|
29
|
+
* generic happy-path tests "high" — so ranking on it inverts the intended order.
|
|
30
|
+
* Priority in this pipeline is derived from the scenario's category and from the
|
|
31
|
+
* verified discriminator, never from the agent's self-assessment.
|
|
32
|
+
*/
|
|
33
|
+
export function rankCandidates(candidates, opts = {}) {
|
|
34
|
+
const carveOutSet = new Set(opts.carveOutCategories ?? DEFAULT_CARVE_OUT_CATEGORIES);
|
|
35
|
+
return [...candidates].sort((a, b) => compareRank(a, b, carveOutSet));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Convenience composition: rank the candidates, split GENERATE vs ADDITIONAL via
|
|
39
|
+
* the diversity-balanced budgeter, and surface the caller-supplied demotions on
|
|
40
|
+
* the result. The caller (the register-plan tool) runs `validateDiscriminator`
|
|
41
|
+
* first — setting `verifiedDiscriminator` on the candidates that passed and
|
|
42
|
+
* passing the failed claims through `ctx.demotions`.
|
|
43
|
+
*/
|
|
44
|
+
export function selectPlan(candidates, ctx, opts = {}) {
|
|
45
|
+
const ranked = rankCandidates(candidates, opts);
|
|
46
|
+
const result = diversityBalancedBudgeter.select(ranked, ctx);
|
|
47
|
+
return { ...result, demotions: ctx.demotions ?? [] };
|
|
48
|
+
}
|
|
49
|
+
function compareRank(a, b, carveOutSet) {
|
|
50
|
+
const carveA = carveOutSet.has(a.scenario?.category) ? 0 : 1;
|
|
51
|
+
const carveB = carveOutSet.has(b.scenario?.category) ? 0 : 1;
|
|
52
|
+
if (carveA !== carveB)
|
|
53
|
+
return carveA - carveB;
|
|
54
|
+
const verifiedA = a.verifiedDiscriminator ? 0 : 1;
|
|
55
|
+
const verifiedB = b.verifiedDiscriminator ? 0 : 1;
|
|
56
|
+
if (verifiedA !== verifiedB)
|
|
57
|
+
return verifiedA - verifiedB;
|
|
58
|
+
const catRankA = categoryRank(a);
|
|
59
|
+
const catRankB = categoryRank(b);
|
|
60
|
+
if (catRankA !== catRankB)
|
|
61
|
+
return catRankA - catRankB;
|
|
62
|
+
return (a.candidateId ?? "").localeCompare(b.candidateId ?? "");
|
|
63
|
+
}
|
|
64
|
+
function categoryRank(candidate) {
|
|
65
|
+
const tier = CATEGORY_PRIORITY[candidate.scenario?.category] ?? PriorityTier.LOW;
|
|
66
|
+
return PRIORITY_RANK[tier];
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { rankCandidates, selectPlan } from "./planRanker.js";
|
|
2
|
+
import { Novelty, PriorityTier } from "../types/TestRecommendation.js";
|
|
3
|
+
import { CandidateSource, DiscriminatorKind, computeCandidateId } from "../types/Recommendation.js";
|
|
4
|
+
function cand(name, opts = {}) {
|
|
5
|
+
const steps = [
|
|
6
|
+
{ order: 0, method: "GET", path: `/${name}`, description: "", interactionType: "success", expectedStatusCode: 200 },
|
|
7
|
+
{ order: 1, method: "POST", path: `/${name}`, description: "", interactionType: "success", expectedStatusCode: 201 },
|
|
8
|
+
];
|
|
9
|
+
const scenario = {
|
|
10
|
+
scenarioName: name,
|
|
11
|
+
description: name,
|
|
12
|
+
category: opts.category ?? "workflow",
|
|
13
|
+
priority: opts.agentPriority ?? "high",
|
|
14
|
+
steps,
|
|
15
|
+
chainingKeys: [],
|
|
16
|
+
requiresAuth: false,
|
|
17
|
+
estimatedComplexity: "moderate",
|
|
18
|
+
testType: opts.testType ?? "integration",
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
scenario,
|
|
22
|
+
priority: PriorityTier.HIGH,
|
|
23
|
+
novelty: Novelty.EXISTING,
|
|
24
|
+
source: CandidateSource.AGENT,
|
|
25
|
+
candidateId: computeCandidateId(scenario),
|
|
26
|
+
verifiedDiscriminator: opts.verified,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const names = (cs) => cs.map((c) => c.scenario.scenarioName);
|
|
30
|
+
const ids = (cs) => cs.map((c) => c.candidateId);
|
|
31
|
+
describe("rankCandidates", () => {
|
|
32
|
+
it("floats a verified discriminator (agent priority 'low') above an unverified security_boundary (agent priority 'high') — the round-1 miscalibration regression", () => {
|
|
33
|
+
const verifiedLow = cand("verified-boundary", {
|
|
34
|
+
category: "business_rule",
|
|
35
|
+
agentPriority: "low",
|
|
36
|
+
verified: DiscriminatorKind.BOUNDARY_EQUALITY,
|
|
37
|
+
});
|
|
38
|
+
const unverifiedHigh = cand("generic-authz", {
|
|
39
|
+
category: "security_boundary",
|
|
40
|
+
agentPriority: "high",
|
|
41
|
+
});
|
|
42
|
+
const ranked = rankCandidates([unverifiedHigh, verifiedLow]);
|
|
43
|
+
expect(names(ranked)).toEqual(["verified-boundary", "generic-authz"]);
|
|
44
|
+
});
|
|
45
|
+
it("keeps bug_caught / CRITICAL carve-out first, even ahead of a verified discriminator", () => {
|
|
46
|
+
const bug = cand("bug-catcher", { category: "bug_caught" });
|
|
47
|
+
const verified = cand("verified-agg", { category: "business_rule", verified: DiscriminatorKind.MULTI_RECORD_AGGREGATE });
|
|
48
|
+
const ranked = rankCandidates([verified, bug]);
|
|
49
|
+
expect(names(ranked)[0]).toBe("bug-catcher");
|
|
50
|
+
});
|
|
51
|
+
it("orders by category priority when carve-out and verification are equal", () => {
|
|
52
|
+
const high = cand("high-cat", { category: "security_boundary" }); // HIGH
|
|
53
|
+
const low = cand("low-cat", { category: "crud" }); // LOW
|
|
54
|
+
const medium = cand("medium-cat", { category: "workflow" }); // MEDIUM
|
|
55
|
+
expect(names(rankCandidates([low, medium, high]))).toEqual(["high-cat", "medium-cat", "low-cat"]);
|
|
56
|
+
});
|
|
57
|
+
it("is deterministic: identical input and any shuffle yield the same order", () => {
|
|
58
|
+
const a = cand("alpha", { category: "business_rule", verified: DiscriminatorKind.BOUNDARY_EQUALITY });
|
|
59
|
+
const b = cand("bravo", { category: "security_boundary" });
|
|
60
|
+
const c = cand("charlie", { category: "bug_caught" });
|
|
61
|
+
const d = cand("delta", { category: "crud", agentPriority: "high" });
|
|
62
|
+
const e = cand("echo", { category: "workflow", verified: DiscriminatorKind.STATE_TRANSITION });
|
|
63
|
+
const order1 = ids(rankCandidates([a, b, c, d, e]));
|
|
64
|
+
const order2 = ids(rankCandidates([a, b, c, d, e]));
|
|
65
|
+
const shuffled = ids(rankCandidates([e, c, a, d, b]));
|
|
66
|
+
expect(order1).toEqual(order2);
|
|
67
|
+
expect(shuffled).toEqual(order1);
|
|
68
|
+
});
|
|
69
|
+
it("does not let the agent priority tag break a tie (candidateId is the stable tiebreak)", () => {
|
|
70
|
+
// Same category, same verification → agent priority is irrelevant; order is by candidateId.
|
|
71
|
+
const hi = cand("zeta", { category: "workflow", agentPriority: "high" });
|
|
72
|
+
const lo = cand("alfa", { category: "workflow", agentPriority: "low" });
|
|
73
|
+
const forward = ids(rankCandidates([hi, lo]));
|
|
74
|
+
const backward = ids(rankCandidates([lo, hi]));
|
|
75
|
+
expect(forward).toEqual(backward);
|
|
76
|
+
});
|
|
77
|
+
it("does not mutate the input array", () => {
|
|
78
|
+
const input = [cand("b", { category: "crud" }), cand("a", { category: "bug_caught" })];
|
|
79
|
+
const before = names(input);
|
|
80
|
+
rankCandidates(input);
|
|
81
|
+
expect(names(input)).toEqual(before);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe("selectPlan", () => {
|
|
85
|
+
const ctx = (over = {}) => ({
|
|
86
|
+
maxGenerate: 3,
|
|
87
|
+
maxTotal: 20,
|
|
88
|
+
isUIOnlyPR: false,
|
|
89
|
+
hasFrontendChanges: false,
|
|
90
|
+
externalCoverage: new Set(),
|
|
91
|
+
...over,
|
|
92
|
+
});
|
|
93
|
+
it("ranks then budgets, surfacing the supplied demotions on the result", () => {
|
|
94
|
+
const candidates = [
|
|
95
|
+
cand("gen-a", { category: "business_rule", verified: DiscriminatorKind.BOUNDARY_EQUALITY }),
|
|
96
|
+
cand("gen-b", { category: "security_boundary" }),
|
|
97
|
+
cand("gen-c", { category: "crud" }),
|
|
98
|
+
];
|
|
99
|
+
const demotions = [{ candidateId: "faked-claim-abc12345", reason: "boundary_equality unverified" }];
|
|
100
|
+
const res = selectPlan(candidates, ctx({ demotions }));
|
|
101
|
+
expect(res.generate.length).toBeGreaterThan(0);
|
|
102
|
+
expect(res.demotions).toEqual(demotions);
|
|
103
|
+
// The verified discriminator is ranked first and lands in GENERATE.
|
|
104
|
+
expect(res.generate[0].scenario.scenarioName).toBe("gen-a");
|
|
105
|
+
});
|
|
106
|
+
it("defaults demotions to an empty array when none are supplied", () => {
|
|
107
|
+
const res = selectPlan([cand("solo", { category: "workflow" })], ctx());
|
|
108
|
+
expect(res.demotions).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ScenarioStep } from "../types/RepositoryAnalysis.js";
|
|
2
|
+
import { Novelty, PriorityTier, ScenarioCategory } from "../types/TestRecommendation.js";
|
|
3
|
+
import { Candidate } from "../types/Recommendation.js";
|
|
4
|
+
/** Build a minimal ScenarioStep. */
|
|
5
|
+
export declare function mkStep(method?: string, path?: string): ScenarioStep;
|
|
6
|
+
interface CandidateOpts {
|
|
7
|
+
priority?: PriorityTier;
|
|
8
|
+
testType?: string;
|
|
9
|
+
steps?: number;
|
|
10
|
+
category?: ScenarioCategory;
|
|
11
|
+
description?: string;
|
|
12
|
+
novelty?: Novelty;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a ranked Candidate for budgeter/ranker tests. Steps default to a
|
|
16
|
+
* distinct path per candidate (so coverage keys don't collide) with the final
|
|
17
|
+
* step as a mutation (so resolvePrimaryStep picks a stable primary step).
|
|
18
|
+
*
|
|
19
|
+
* Note the two distinct priority fields: `scenario.priority` is the agent's
|
|
20
|
+
* own drafted label (lowercase — untrusted, ranking never reads it), while
|
|
21
|
+
* `Candidate.priority` is the server-computed PriorityTier the rankers
|
|
22
|
+
* actually order by.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mkCandidate(name: string, opts?: CandidateOpts): Candidate;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Novelty, PriorityTier, } from "../types/TestRecommendation.js";
|
|
2
|
+
import { CandidateSource, computeCandidateId, } from "../types/Recommendation.js";
|
|
3
|
+
/** Build a minimal ScenarioStep. */
|
|
4
|
+
export function mkStep(method = "GET", path = "/r") {
|
|
5
|
+
return {
|
|
6
|
+
order: 0,
|
|
7
|
+
method,
|
|
8
|
+
path,
|
|
9
|
+
description: "",
|
|
10
|
+
interactionType: "success",
|
|
11
|
+
expectedStatusCode: 200,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a ranked Candidate for budgeter/ranker tests. Steps default to a
|
|
16
|
+
* distinct path per candidate (so coverage keys don't collide) with the final
|
|
17
|
+
* step as a mutation (so resolvePrimaryStep picks a stable primary step).
|
|
18
|
+
*
|
|
19
|
+
* Note the two distinct priority fields: `scenario.priority` is the agent's
|
|
20
|
+
* own drafted label (lowercase — untrusted, ranking never reads it), while
|
|
21
|
+
* `Candidate.priority` is the server-computed PriorityTier the rankers
|
|
22
|
+
* actually order by.
|
|
23
|
+
*/
|
|
24
|
+
export function mkCandidate(name, opts = {}) {
|
|
25
|
+
const stepCount = opts.steps ?? (opts.testType === "contract" ? 1 : 2);
|
|
26
|
+
const steps = Array.from({ length: stepCount }, (_, i) => mkStep(i === stepCount - 1 ? "POST" : "GET", `/${name}`));
|
|
27
|
+
const scenario = {
|
|
28
|
+
scenarioName: name,
|
|
29
|
+
description: opts.description ?? name,
|
|
30
|
+
category: opts.category ?? "workflow",
|
|
31
|
+
priority: "high",
|
|
32
|
+
steps,
|
|
33
|
+
chainingKeys: [],
|
|
34
|
+
requiresAuth: false,
|
|
35
|
+
estimatedComplexity: "moderate",
|
|
36
|
+
...(opts.testType ? { testType: opts.testType } : {}),
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
scenario,
|
|
40
|
+
priority: opts.priority ?? PriorityTier.HIGH,
|
|
41
|
+
novelty: opts.novelty ?? Novelty.EXISTING,
|
|
42
|
+
source: CandidateSource.AGENT,
|
|
43
|
+
candidateId: computeCandidateId(scenario),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -25,7 +25,10 @@ export function registerTestbotResource(server) {
|
|
|
25
25
|
const maxCrit = parseInt(uri.searchParams.get("maxCritical") || "", 10);
|
|
26
26
|
const repositoryPath = param("repositoryPath", ".");
|
|
27
27
|
const services = await readWorkspaceServices(repositoryPath);
|
|
28
|
-
const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined, parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined), uri.searchParams.get("primaryRepo") || undefined
|
|
28
|
+
const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined, parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined), uri.searchParams.get("primaryRepo") || undefined,
|
|
29
|
+
// Plan-only eval lane (SKYR-3879): recommendation phase only, nothing
|
|
30
|
+
// generated or executed. Same accepted spellings as the prompt schema.
|
|
31
|
+
["true", "1"].includes(uri.searchParams.get("planOnly") ?? ""));
|
|
29
32
|
AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
|
|
30
33
|
// Return the original URI — clients may use it to re-fetch the resource,
|
|
31
34
|
// and the caller already has these params. Credentials never appear in
|
|
@@ -12,6 +12,8 @@ export interface TraceRequest {
|
|
|
12
12
|
Port: number;
|
|
13
13
|
Timestamp: string;
|
|
14
14
|
Scheme: string;
|
|
15
|
+
UniqueFields?: string[];
|
|
16
|
+
scenarioName?: string;
|
|
15
17
|
}
|
|
16
18
|
export interface ScenarioParams {
|
|
17
19
|
scenarioName: string;
|
|
@@ -29,7 +31,10 @@ export interface ScenarioParams {
|
|
|
29
31
|
authScheme?: string;
|
|
30
32
|
authToken?: string;
|
|
31
33
|
responseHeaders?: Record<string, string[]>;
|
|
34
|
+
uniqueFields?: string[];
|
|
32
35
|
}
|
|
33
36
|
export declare class ScenarioGenerationService {
|
|
37
|
+
private lastTimestampMs;
|
|
38
|
+
private nextTimestamp;
|
|
34
39
|
generateTraceRequestFromInput(params: ScenarioParams): TraceRequest | null;
|
|
35
40
|
}
|
|
@@ -7,6 +7,17 @@ import { logger } from "../utils/logger.js";
|
|
|
7
7
|
// LLM-controlled or user-controlled JSON input.
|
|
8
8
|
const PROTO_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
9
9
|
export class ScenarioGenerationService {
|
|
10
|
+
// Trace timestamps must strictly increase in generation order: the Go
|
|
11
|
+
// codegen treats "earlier timestamp" as "prior step" when chaining
|
|
12
|
+
// FKs/path params from create responses, and all steps of a batch are
|
|
13
|
+
// generated within the same millisecond (SKYR-3884). The service owns the
|
|
14
|
+
// invariant — each trace from this instance is stamped at least one second
|
|
15
|
+
// after the previous one — so no caller can forget to thread an index.
|
|
16
|
+
lastTimestampMs = 0;
|
|
17
|
+
nextTimestamp() {
|
|
18
|
+
this.lastTimestampMs = Math.max(Date.now(), this.lastTimestampMs + 1000);
|
|
19
|
+
return new Date(this.lastTimestampMs).toISOString();
|
|
20
|
+
}
|
|
10
21
|
generateTraceRequestFromInput(params) {
|
|
11
22
|
let destination = params.destination;
|
|
12
23
|
let scheme = "https";
|
|
@@ -30,7 +41,7 @@ export class ScenarioGenerationService {
|
|
|
30
41
|
});
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
|
-
const timestamp =
|
|
44
|
+
const timestamp = this.nextTimestamp();
|
|
34
45
|
const method = params.method;
|
|
35
46
|
const statusCode = params.statusCode ?? inferExpectedStatus(method);
|
|
36
47
|
const requestBody = params.requestBody ||
|
|
@@ -118,6 +129,10 @@ export class ScenarioGenerationService {
|
|
|
118
129
|
Port: port,
|
|
119
130
|
Timestamp: timestamp,
|
|
120
131
|
Scheme: scheme,
|
|
132
|
+
scenarioName: params.scenarioName,
|
|
133
|
+
...(params.uniqueFields && params.uniqueFields.length > 0
|
|
134
|
+
? { UniqueFields: params.uniqueFields }
|
|
135
|
+
: {}),
|
|
121
136
|
};
|
|
122
137
|
}
|
|
123
138
|
}
|
|
@@ -368,3 +368,47 @@ describe("ScenarioGenerationService — baseURL parsing", () => {
|
|
|
368
368
|
expect(trace.Port).toBe(80);
|
|
369
369
|
});
|
|
370
370
|
});
|
|
371
|
+
describe("ScenarioGenerationService — unique fields (SKYR-3882)", () => {
|
|
372
|
+
it("carries uniqueFields onto the trace request", () => {
|
|
373
|
+
const trace = generateTrace({ uniqueFields: ["name", "slug"] });
|
|
374
|
+
expect(trace).not.toBeNull();
|
|
375
|
+
expect(trace.UniqueFields).toEqual(["name", "slug"]);
|
|
376
|
+
});
|
|
377
|
+
it("omits UniqueFields when none are provided", () => {
|
|
378
|
+
const trace = generateTrace({});
|
|
379
|
+
expect(trace).not.toBeNull();
|
|
380
|
+
expect(trace.UniqueFields).toBeUndefined();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
describe("ScenarioGenerationService — step timestamps (SKYR-3884)", () => {
|
|
384
|
+
it("stamps consecutive traces from one instance with strictly increasing timestamps", () => {
|
|
385
|
+
// All steps of a batch are generated within the same millisecond, so a bare
|
|
386
|
+
// new Date().toISOString() gives every step the identical timestamp. The Go
|
|
387
|
+
// codegen treats "earlier timestamp" as "prior step" when chaining FKs and
|
|
388
|
+
// path params from create responses — equal stamps break every chain. The
|
|
389
|
+
// service owns the invariant, so callers cannot forget to thread an index.
|
|
390
|
+
const service = new ScenarioGenerationService();
|
|
391
|
+
const traces = [0, 1, 2].map(() => service.generateTraceRequestFromInput(BASE_PARAMS));
|
|
392
|
+
for (let i = 1; i < traces.length; i++) {
|
|
393
|
+
expect(new Date(traces[i].Timestamp).getTime()).toBeGreaterThan(new Date(traces[i - 1].Timestamp).getTime());
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
it("stamps the first trace of a fresh instance at the current time", () => {
|
|
397
|
+
const before = Date.now();
|
|
398
|
+
const trace = generateTrace({});
|
|
399
|
+
const after = Date.now();
|
|
400
|
+
const ts = new Date(trace.Timestamp).getTime();
|
|
401
|
+
expect(ts).toBeGreaterThanOrEqual(before);
|
|
402
|
+
expect(ts).toBeLessThanOrEqual(after);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
describe("ScenarioGenerationService — scenarioName embedding (SKYR-3884)", () => {
|
|
406
|
+
it("writes scenarioName into each trace request so the plan guard can identify the file", () => {
|
|
407
|
+
// readScenarioNameFromFile (generateIntegrationRestTool) reads
|
|
408
|
+
// parsed[0].scenarioName to match the file against the approved plan.
|
|
409
|
+
// Without this field every scenarioFile-mode generation is rejected
|
|
410
|
+
// fail-closed whenever a plan is active.
|
|
411
|
+
const trace = generateTrace({});
|
|
412
|
+
expect(trace.scenarioName).toBe("test-scenario");
|
|
413
|
+
});
|
|
414
|
+
});
|