@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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/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 +98 -52
- 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 +3 -0
- package/build/tools/code-refactor/codeReuseTool.js +76 -4
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
- package/build/tools/code-refactor/codeReuseTool.test.js +51 -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
|
@@ -378,3 +378,47 @@ describe("parseRelatedRepositories", () => {
|
|
|
378
378
|
expect(parsed).toEqual([{ repo: "o/ok", repositoryPath: "/ok", baseBranch: undefined }]);
|
|
379
379
|
});
|
|
380
380
|
});
|
|
381
|
+
describe("plan-only eval mode (via getTestbotPrompt)", () => {
|
|
382
|
+
function callWithPlanOnly(planOnly) {
|
|
383
|
+
return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
|
|
384
|
+
undefined, // maxRecommendations
|
|
385
|
+
undefined, // maxGenerate
|
|
386
|
+
undefined, // maxCritical
|
|
387
|
+
undefined, // prNumber
|
|
388
|
+
undefined, // userPrompt
|
|
389
|
+
undefined, // services
|
|
390
|
+
undefined, // uiCredentials
|
|
391
|
+
undefined, // testsRepoDir
|
|
392
|
+
undefined, // relatedRepositories
|
|
393
|
+
undefined, // primaryRepo
|
|
394
|
+
planOnly);
|
|
395
|
+
}
|
|
396
|
+
it("branches Task 2 to the plan-only variant when planOnly is true", () => {
|
|
397
|
+
const prompt = callWithPlanOnly(true);
|
|
398
|
+
expect(prompt).toContain("## Task 2: Commit the Test Plan (plan-only run)");
|
|
399
|
+
expect(prompt).not.toContain("## Task 2: Generate New Tests");
|
|
400
|
+
// The standard generation/execution mandates must be absent, not overridden.
|
|
401
|
+
expect(prompt).not.toContain("skyramp_integration_test_generation");
|
|
402
|
+
expect(prompt).not.toContain('phase: "before"');
|
|
403
|
+
// Task 3 declares the plan instead of counting generated files.
|
|
404
|
+
expect(prompt).toContain("DECLARES your final GENERATE selection");
|
|
405
|
+
});
|
|
406
|
+
it("renders the standard prompt untouched when planOnly is false or omitted", () => {
|
|
407
|
+
const prompt = callWithPlanOnly(false);
|
|
408
|
+
expect(prompt).toContain("## Task 2: Generate New Tests");
|
|
409
|
+
expect(prompt).not.toContain("plan-only");
|
|
410
|
+
expect(prompt).toBe(callWithServices([]));
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
describe("UI code reuse wiring", () => {
|
|
414
|
+
it("instructs codeReuse: true for TS/JS Playwright UI generation", () => {
|
|
415
|
+
const p = callWithServices([]);
|
|
416
|
+
expect(p).toContain("codeReuse: true");
|
|
417
|
+
});
|
|
418
|
+
it("teaches both reuse outcomes and the raw.bak insurance rule", () => {
|
|
419
|
+
const p = callWithServices([]);
|
|
420
|
+
expect(p).toContain("No reusable POM layer detected");
|
|
421
|
+
expect(p).toContain("verification: passed");
|
|
422
|
+
expect(p).toContain(".raw.bak");
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Budgeter } from "../../types/Recommendation.js";
|
|
2
|
+
/**
|
|
3
|
+
* Budgeter that guarantees cross-test-type coverage in the GENERATE set.
|
|
4
|
+
* `maxGenerate` acts as an upper cap (not a hard N). Fixes the single-type skew
|
|
5
|
+
* that starves contract/error-path tests. Opt-in via strategy selection.
|
|
6
|
+
*/
|
|
7
|
+
export declare const diversityBalancedBudgeter: Budgeter;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { runBudget } from "./shared.js";
|
|
2
|
+
import { bucketByType, inferScenarioType, isProtectedCandidate, roundRobinFill, } from "../diversity.js";
|
|
3
|
+
const typeOf = (c) => inferScenarioType(c.scenario);
|
|
4
|
+
/**
|
|
5
|
+
* Pick `count` items guaranteeing each present test type at least one slot.
|
|
6
|
+
*
|
|
7
|
+
* Unlike roundRobinByType (which fills ALL protected items first and can
|
|
8
|
+
* starve other types at small N — the SKYR-3879 "0 contract executed" skew),
|
|
9
|
+
* this caps protected occupancy to `count - (numTypes - 1)` so at least one slot
|
|
10
|
+
* per remaining type is always reachable, then:
|
|
11
|
+
* Phase 1 — one item for each still-uncovered present type (by rank), then
|
|
12
|
+
* Phase 2 — round-robin the remainder across types until `count` is reached.
|
|
13
|
+
* Rank order is preserved within each type; degenerate cases match a rank slice.
|
|
14
|
+
*/
|
|
15
|
+
function floorBalancedPick(items, count) {
|
|
16
|
+
if (count <= 0)
|
|
17
|
+
return [];
|
|
18
|
+
if (count >= items.length)
|
|
19
|
+
return items.slice(0, count);
|
|
20
|
+
// Present types in first-appearance (rank) order.
|
|
21
|
+
const typesOrder = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const it of items) {
|
|
24
|
+
const t = typeOf(it);
|
|
25
|
+
if (!seen.has(t)) {
|
|
26
|
+
seen.add(t);
|
|
27
|
+
typesOrder.push(t);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const maxProtected = Math.max(0, count - (typesOrder.length - 1));
|
|
31
|
+
const selected = [];
|
|
32
|
+
const pool = [];
|
|
33
|
+
let protectedTaken = 0;
|
|
34
|
+
for (const it of items) {
|
|
35
|
+
if (isProtectedCandidate(it.priority, it.scenario) &&
|
|
36
|
+
protectedTaken < maxProtected &&
|
|
37
|
+
selected.length < count) {
|
|
38
|
+
selected.push(it);
|
|
39
|
+
protectedTaken++;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
pool.push(it);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const { buckets } = bucketByType(pool, typeOf);
|
|
46
|
+
const covered = new Set(selected.map(typeOf));
|
|
47
|
+
// Phase 1 — floor: one item per still-uncovered present type.
|
|
48
|
+
for (const t of typesOrder) {
|
|
49
|
+
if (selected.length >= count)
|
|
50
|
+
break;
|
|
51
|
+
if (covered.has(t))
|
|
52
|
+
continue;
|
|
53
|
+
const bucket = buckets.get(t);
|
|
54
|
+
if (bucket && bucket.length > 0) {
|
|
55
|
+
selected.push(bucket.shift());
|
|
56
|
+
covered.add(t);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Phase 2 — round-robin the remainder across types until full.
|
|
60
|
+
roundRobinFill(selected, typesOrder, buckets, count);
|
|
61
|
+
return selected;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Budgeter that guarantees cross-test-type coverage in the GENERATE set.
|
|
65
|
+
* `maxGenerate` acts as an upper cap (not a hard N). Fixes the single-type skew
|
|
66
|
+
* that starves contract/error-path tests. Opt-in via strategy selection.
|
|
67
|
+
*/
|
|
68
|
+
export const diversityBalancedBudgeter = {
|
|
69
|
+
name: "diversity-balanced",
|
|
70
|
+
select: (ranked, ctx) => runBudget(ranked, ctx, floorBalancedPick),
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { diversityBalancedBudgeter } from "./diversityBalancedBudgeter.js";
|
|
2
|
+
import { PriorityTier } from "../../types/TestRecommendation.js";
|
|
3
|
+
import { fixedNBudgeter } from "./fixedNBudgeter.js";
|
|
4
|
+
import { mkCandidate } from "../testFixtures.js";
|
|
5
|
+
function ctx(over = {}) {
|
|
6
|
+
return {
|
|
7
|
+
maxGenerate: 3,
|
|
8
|
+
maxTotal: 20,
|
|
9
|
+
isUIOnlyPR: false,
|
|
10
|
+
hasFrontendChanges: false,
|
|
11
|
+
externalCoverage: new Set(),
|
|
12
|
+
...over,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const countType = (r, t) => r.generate.filter((c) => c.scenario.testType === t).length;
|
|
16
|
+
const typesIn = (r) => new Set(r.generate.map((c) => c.scenario.testType));
|
|
17
|
+
describe("diversityBalancedBudgeter", () => {
|
|
18
|
+
it("guarantees a contract slot where fixed-n starves it (the SKYR-3879 fix)", () => {
|
|
19
|
+
// Protected (CRITICAL) integration scenarios out-rank contract ones and, at N=3,
|
|
20
|
+
// fixed-n's protected-first fill consumes every slot with integration → 0 contract.
|
|
21
|
+
const ranked = [
|
|
22
|
+
mkCandidate("i1", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
23
|
+
mkCandidate("i2", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
24
|
+
mkCandidate("i3", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
25
|
+
mkCandidate("c1", { testType: "contract" }),
|
|
26
|
+
mkCandidate("c2", { testType: "contract" }),
|
|
27
|
+
];
|
|
28
|
+
const c = ctx({ maxGenerate: 3 });
|
|
29
|
+
// Baseline: fixed-n reproduces the skew.
|
|
30
|
+
expect(countType(fixedNBudgeter.select(ranked, c), "contract")).toBe(0);
|
|
31
|
+
// Fix: diversity-balanced reserves a per-type floor.
|
|
32
|
+
const div = diversityBalancedBudgeter.select(ranked, c);
|
|
33
|
+
expect(div.generate.length).toBe(3);
|
|
34
|
+
expect(countType(div, "contract")).toBeGreaterThanOrEqual(1);
|
|
35
|
+
expect(countType(div, "integration")).toBeGreaterThanOrEqual(1);
|
|
36
|
+
});
|
|
37
|
+
it("gives every present test type at least one slot before any type gets a second", () => {
|
|
38
|
+
const ranked = [
|
|
39
|
+
mkCandidate("i1", { testType: "integration" }),
|
|
40
|
+
mkCandidate("i2", { testType: "integration" }),
|
|
41
|
+
mkCandidate("i3", { testType: "integration" }),
|
|
42
|
+
mkCandidate("c1", { testType: "contract" }),
|
|
43
|
+
mkCandidate("c2", { testType: "contract" }),
|
|
44
|
+
mkCandidate("u1", { testType: "ui" }),
|
|
45
|
+
];
|
|
46
|
+
const div = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 3 }));
|
|
47
|
+
expect(typesIn(div)).toEqual(new Set(["integration", "contract", "ui"]));
|
|
48
|
+
});
|
|
49
|
+
it("raising maxGenerate increases per-type coverage while keeping the floor", () => {
|
|
50
|
+
const ranked = [
|
|
51
|
+
mkCandidate("i1", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
52
|
+
mkCandidate("i2", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
53
|
+
mkCandidate("i3", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
54
|
+
mkCandidate("i4", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
55
|
+
mkCandidate("i5", { testType: "integration", priority: PriorityTier.CRITICAL }),
|
|
56
|
+
mkCandidate("c1", { testType: "contract" }),
|
|
57
|
+
mkCandidate("c2", { testType: "contract" }),
|
|
58
|
+
mkCandidate("u1", { testType: "ui" }),
|
|
59
|
+
];
|
|
60
|
+
const atThree = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 3 }));
|
|
61
|
+
const atSix = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 6 }));
|
|
62
|
+
expect(countType(atThree, "contract")).toBeGreaterThanOrEqual(1);
|
|
63
|
+
expect(countType(atThree, "ui")).toBeGreaterThanOrEqual(1);
|
|
64
|
+
expect(countType(atSix, "contract")).toBeGreaterThanOrEqual(1);
|
|
65
|
+
expect(countType(atSix, "ui")).toBeGreaterThanOrEqual(1);
|
|
66
|
+
// more budget → more of the highest-ranked (integration) coverage
|
|
67
|
+
expect(countType(atSix, "integration")).toBeGreaterThan(countType(atThree, "integration"));
|
|
68
|
+
expect(atSix.generate.length).toBe(6);
|
|
69
|
+
});
|
|
70
|
+
it("returns everything (rank order) when maxGenerate >= candidate count", () => {
|
|
71
|
+
const ranked = [mkCandidate("i1", { testType: "integration" }), mkCandidate("c1", { testType: "contract" })];
|
|
72
|
+
const div = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 5 }));
|
|
73
|
+
expect(div.generate.map((c) => c.scenario.scenarioName)).toEqual(["i1", "c1"]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Budgeter } from "../../types/Recommendation.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default budgeter — reproduces the pre-refactor behavior exactly: fill up to
|
|
4
|
+
* `maxGenerate` GENERATE slots by round-robin across the present test types
|
|
5
|
+
* (protected items first), the rest become ADDITIONAL.
|
|
6
|
+
*/
|
|
7
|
+
export declare const fixedNBudgeter: Budgeter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { roundRobinByType } from "../diversity.js";
|
|
2
|
+
import { runBudget } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Default budgeter — reproduces the pre-refactor behavior exactly: fill up to
|
|
5
|
+
* `maxGenerate` GENERATE slots by round-robin across the present test types
|
|
6
|
+
* (protected items first), the rest become ADDITIONAL.
|
|
7
|
+
*/
|
|
8
|
+
export const fixedNBudgeter = {
|
|
9
|
+
name: "fixed-n",
|
|
10
|
+
select: (ranked, ctx) => runBudget(ranked, ctx, roundRobinByType),
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fixedNBudgeter } from "./fixedNBudgeter.js";
|
|
2
|
+
import { mkCandidate } from "../testFixtures.js";
|
|
3
|
+
import { externalDedupKey } from "../../prompts/test-recommendation/recommendationShared.js";
|
|
4
|
+
function ctx(over = {}) {
|
|
5
|
+
return {
|
|
6
|
+
maxGenerate: 3,
|
|
7
|
+
maxTotal: 20,
|
|
8
|
+
isUIOnlyPR: false,
|
|
9
|
+
hasFrontendChanges: false,
|
|
10
|
+
externalCoverage: new Set(),
|
|
11
|
+
...over,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const gen = (r) => r.generate.map((c) => c.scenario.scenarioName);
|
|
15
|
+
describe("fixedNBudgeter", () => {
|
|
16
|
+
it("backend-only PR fills maxGenerate slots via round-robin across test types", () => {
|
|
17
|
+
const ranked = [
|
|
18
|
+
mkCandidate("i1", { testType: "integration" }),
|
|
19
|
+
mkCandidate("i2", { testType: "integration" }),
|
|
20
|
+
mkCandidate("i3", { testType: "integration" }),
|
|
21
|
+
mkCandidate("c1", { testType: "contract" }),
|
|
22
|
+
mkCandidate("c2", { testType: "contract" }),
|
|
23
|
+
];
|
|
24
|
+
const res = fixedNBudgeter.select(ranked, ctx());
|
|
25
|
+
expect(res.generate.length).toBe(3);
|
|
26
|
+
expect(res.reservedUISlots).toBe(0);
|
|
27
|
+
// buckets [integration, contract]; round 1 → i1, c1; round 2 → i2
|
|
28
|
+
expect(gen(res)).toEqual(["i1", "c1", "i2"]);
|
|
29
|
+
});
|
|
30
|
+
it("mixed (frontend) PR reserves one UI slot → backend count = maxGenerate - 1", () => {
|
|
31
|
+
const ranked = [
|
|
32
|
+
mkCandidate("i1", { testType: "integration" }),
|
|
33
|
+
mkCandidate("i2", { testType: "integration" }),
|
|
34
|
+
mkCandidate("c1", { testType: "contract" }),
|
|
35
|
+
];
|
|
36
|
+
const res = fixedNBudgeter.select(ranked, ctx({ hasFrontendChanges: true }));
|
|
37
|
+
expect(res.generate.length).toBe(2);
|
|
38
|
+
expect(res.reservedUISlots).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
it("UI-only PR selects no backend generate items", () => {
|
|
41
|
+
const res = fixedNBudgeter.select([mkCandidate("i1", { testType: "integration" })], ctx({ isUIOnlyPR: true }));
|
|
42
|
+
expect(res.generate.length).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
it("additional = leftover candidates (rank order), excluding anything generated", () => {
|
|
45
|
+
const ranked = [
|
|
46
|
+
mkCandidate("i1", { testType: "integration" }),
|
|
47
|
+
mkCandidate("c1", { testType: "contract" }),
|
|
48
|
+
mkCandidate("i2", { testType: "integration" }),
|
|
49
|
+
mkCandidate("c2", { testType: "contract" }),
|
|
50
|
+
];
|
|
51
|
+
const res = fixedNBudgeter.select(ranked, ctx());
|
|
52
|
+
const generated = new Set(gen(res));
|
|
53
|
+
for (const a of res.additional) {
|
|
54
|
+
expect(generated.has(a.scenario.scenarioName)).toBe(false);
|
|
55
|
+
}
|
|
56
|
+
expect(res.additional.length).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
it("drops an ordinary candidate already covered by an external test", () => {
|
|
59
|
+
const covered = mkCandidate("covered", { testType: "contract" });
|
|
60
|
+
const fresh = mkCandidate("fresh", { testType: "integration" });
|
|
61
|
+
const res = fixedNBudgeter.select([covered, fresh], ctx({ maxGenerate: 5, externalCoverage: new Set([externalDedupKey(covered.scenario)]) }));
|
|
62
|
+
const all = [...res.generate, ...res.additional].map((c) => c.scenario.scenarioName);
|
|
63
|
+
expect(all).toContain("fresh");
|
|
64
|
+
expect(all).not.toContain("covered");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Candidate, BudgetContext, SelectionResult } from "../../types/Recommendation.js";
|
|
2
|
+
/**
|
|
3
|
+
* Backend GENERATE slot count:
|
|
4
|
+
* - UI-only PR: 0 (all slots are UI placeholders)
|
|
5
|
+
* - Mixed PR: maxGenerate - 1 (last slot reserved for a UI placeholder)
|
|
6
|
+
* - Backend-only PR: maxGenerate
|
|
7
|
+
*/
|
|
8
|
+
export declare function backendGenerateCount(ctx: BudgetContext): number;
|
|
9
|
+
/** UI placeholder slots the render layer fills (UI-only → all; mixed → one). */
|
|
10
|
+
export declare function reservedUISlots(ctx: BudgetContext): number;
|
|
11
|
+
/**
|
|
12
|
+
* Shared budgeting pipeline. All Budgeters run the same external-dedup,
|
|
13
|
+
* attack-surface prioritization, and ADDITIONAL set-difference; they differ
|
|
14
|
+
* ONLY in how they pick the GENERATE items from the slot-ordered list (`pick`).
|
|
15
|
+
*
|
|
16
|
+
* With `pick = roundRobinByType` this reproduces the pre-refactor selection in
|
|
17
|
+
* diffExecutionPlan.ts exactly.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runBudget(ranked: Candidate[], ctx: BudgetContext, pick: (items: Candidate[], count: number) => Candidate[]): SelectionResult;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { prioritizeAttackSurfaceBundles } from "../diversity.js";
|
|
2
|
+
import { externalDedupKey, scenarioCoverageKey, isAttackSurfaceSecurityBoundary, } from "../../prompts/test-recommendation/recommendationShared.js";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Backend GENERATE slot count:
|
|
6
|
+
* - UI-only PR: 0 (all slots are UI placeholders)
|
|
7
|
+
* - Mixed PR: maxGenerate - 1 (last slot reserved for a UI placeholder)
|
|
8
|
+
* - Backend-only PR: maxGenerate
|
|
9
|
+
*/
|
|
10
|
+
export function backendGenerateCount(ctx) {
|
|
11
|
+
if (ctx.isUIOnlyPR)
|
|
12
|
+
return 0;
|
|
13
|
+
return ctx.hasFrontendChanges ? Math.max(0, ctx.maxGenerate - 1) : ctx.maxGenerate;
|
|
14
|
+
}
|
|
15
|
+
/** UI placeholder slots the render layer fills (UI-only → all; mixed → one). */
|
|
16
|
+
export function reservedUISlots(ctx) {
|
|
17
|
+
if (ctx.isUIOnlyPR)
|
|
18
|
+
return ctx.maxGenerate;
|
|
19
|
+
return ctx.hasFrontendChanges && ctx.maxGenerate > 0 ? 1 : 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Drop candidates whose method-aware resource+type is already covered by an
|
|
23
|
+
* external test — except protected `bug_caught` / attack-surface scenarios,
|
|
24
|
+
* which require semantic flaw coverage the external test may not provide.
|
|
25
|
+
*/
|
|
26
|
+
function applyExternalDedup(ranked, externalCoverage) {
|
|
27
|
+
if (externalCoverage.size === 0)
|
|
28
|
+
return ranked;
|
|
29
|
+
return ranked.filter((item) => {
|
|
30
|
+
const key = externalDedupKey(item.scenario);
|
|
31
|
+
if (externalCoverage.has(key)) {
|
|
32
|
+
if (item.scenario.category === "bug_caught" || isAttackSurfaceSecurityBoundary(item.scenario)) {
|
|
33
|
+
logger.info(`External dedup: preserving "${item.scenario.scenarioName}" (${key}) — protected bug/attack-surface scenario requires semantic flaw coverage`);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
logger.info(`External dedup: skipping "${item.scenario.scenarioName}" (${key}) — covered by external test`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Shared budgeting pipeline. All Budgeters run the same external-dedup,
|
|
44
|
+
* attack-surface prioritization, and ADDITIONAL set-difference; they differ
|
|
45
|
+
* ONLY in how they pick the GENERATE items from the slot-ordered list (`pick`).
|
|
46
|
+
*
|
|
47
|
+
* With `pick = roundRobinByType` this reproduces the pre-refactor selection in
|
|
48
|
+
* diffExecutionPlan.ts exactly.
|
|
49
|
+
*/
|
|
50
|
+
export function runBudget(ranked, ctx, pick) {
|
|
51
|
+
const backend = backendGenerateCount(ctx);
|
|
52
|
+
const deduped = applyExternalDedup(ranked, ctx.externalCoverage);
|
|
53
|
+
const slotOrdered = prioritizeAttackSurfaceBundles(deduped);
|
|
54
|
+
const generate = pick(slotOrdered, Math.min(backend, slotOrdered.length));
|
|
55
|
+
// ADDITIONAL = everything not chosen for GENERATE, in rank order, capped at the
|
|
56
|
+
// remaining budget, minus any whose resource+type is already covered by GENERATE.
|
|
57
|
+
const generateSet = new Set(generate);
|
|
58
|
+
const generatedCoverage = new Set(generate.map((item) => scenarioCoverageKey(item.scenario)));
|
|
59
|
+
const additional = slotOrdered
|
|
60
|
+
.filter((it) => !generateSet.has(it))
|
|
61
|
+
.slice(0, Math.max(0, ctx.maxTotal - backend))
|
|
62
|
+
.filter((item) => !generatedCoverage.has(scenarioCoverageKey(item.scenario)));
|
|
63
|
+
// Budgeting itself produces no demotions; the register-plan selection stage
|
|
64
|
+
// (planRanker.selectPlan) fills this channel from discriminator verification.
|
|
65
|
+
return { generate, additional, reservedUISlots: reservedUISlots(ctx), demotions: [] };
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DraftedScenario } from "../types/RepositoryAnalysis.js";
|
|
2
|
+
import { DiscriminatorKind } from "../types/Recommendation.js";
|
|
3
|
+
/**
|
|
4
|
+
* A discriminator claim the agent (or server) attaches to a candidate: the
|
|
5
|
+
* structural bug-shape (`kind`) plus the verbatim diff snippet the test probes.
|
|
6
|
+
*/
|
|
7
|
+
export interface DiscriminatorClaim {
|
|
8
|
+
kind: DiscriminatorKind;
|
|
9
|
+
changedCodeAnchor: string;
|
|
10
|
+
}
|
|
11
|
+
/** Result of verifying a claim against a scenario's steps and the PR diff. */
|
|
12
|
+
export interface DiscriminatorValidation {
|
|
13
|
+
verified: boolean;
|
|
14
|
+
/** One-sentence, human-readable reason present ONLY when `verified` is false;
|
|
15
|
+
* it is returned to the LLM to help it fix the claim. */
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Verify a declared discriminator against the candidate's `steps[]` and the PR
|
|
20
|
+
* diff. Mirrors the classification precedent (`recommendationShared.ts`): the
|
|
21
|
+
* claim is checked STRUCTURALLY, never by fuzzy keyword scans of prose. A failed
|
|
22
|
+
* check returns `verified: false` with a one-sentence reason (the claim is
|
|
23
|
+
* demoted, never used to reject the candidate). NEVER throws — malformed or
|
|
24
|
+
* partial input is treated as unverified.
|
|
25
|
+
*
|
|
26
|
+
* Two gates, both required to verify:
|
|
27
|
+
* 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
|
|
28
|
+
* occurring verbatim in `diffText` — grounds the claim in the real change.
|
|
29
|
+
* 2. The `kind`-specific structural predicate must hold over `steps[]`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateDiscriminator(scenario: DraftedScenario, declared: DiscriminatorClaim, diffText: string): DiscriminatorValidation;
|