@skyramp/mcp 0.3.4 → 0.3.6-rc.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/playwright/registerPlaywrightTools.js +92 -30
- package/build/playwright/traceRecordingPrompt.d.ts +6 -0
- package/build/playwright/traceRecordingPrompt.js +6 -2
- package/build/prompts/code-reuse.d.ts +1 -2
- package/build/prompts/code-reuse.js +182 -77
- package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
- package/build/prompts/modularization/integration-test-modularization.js +83 -41
- package/build/prompts/modularization/render.d.ts +18 -0
- package/build/prompts/modularization/render.js +12 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
- package/build/prompts/modularization/ui-test-modularization.js +89 -47
- package/build/prompts/pom-aware-code-reuse.js +3 -1
- package/build/prompts/shared-helper-policy.d.ts +57 -0
- package/build/prompts/shared-helper-policy.js +135 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
- package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
- package/build/prompts/test-recommendation/recommendationShared.js +90 -16
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +88 -33
- package/build/recommendation/budgeters/shared.js +105 -27
- package/build/recommendation/discriminators.js +13 -2
- package/build/recommendation/planRanker.d.ts +6 -6
- package/build/recommendation/planRanker.js +6 -61
- package/build/services/AnalyticsService.d.ts +7 -0
- package/build/services/AnalyticsService.js +7 -1
- package/build/services/ModularizationService.js +1 -3
- package/build/services/TestDiscoveryService.d.ts +0 -2
- package/build/services/TestDiscoveryService.js +2 -37
- package/build/services/TestGenerationService.d.ts +16 -0
- package/build/services/TestGenerationService.js +86 -10
- package/build/services/containerEnv.js +13 -12
- package/build/tools/code-refactor/codeReuseTool.js +279 -93
- package/build/tools/code-refactor/enhance-state.d.ts +49 -0
- package/build/tools/code-refactor/enhance-state.js +109 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
- package/build/tools/code-refactor/modularizationTool.js +9 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +14 -4
- package/build/tools/code-refactor/reuse-state.d.ts +127 -5
- package/build/tools/code-refactor/reuse-state.js +628 -16
- package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
- package/build/tools/code-refactor/utils-verify-gates.js +100 -0
- package/build/tools/code-refactor/verify-gates.d.ts +2 -1
- package/build/tools/code-refactor/verify-gates.js +90 -25
- package/build/tools/executeSkyrampTestTool.d.ts +19 -0
- package/build/tools/executeSkyrampTestTool.js +158 -8
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateUIRestTool.js +22 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
- package/build/tools/generate-tests/scenarioLint.js +127 -19
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
- package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
- package/build/tools/submitReportTool.d.ts +38 -38
- package/build/tools/submitReportTool.js +487 -104
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +75 -12
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
- package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
- package/build/tools/test-management/registerTestPlanTool.js +149 -23
- package/build/types/Recommendation.d.ts +34 -5
- package/build/types/RepositoryAnalysis.d.ts +133 -114
- package/build/types/RepositoryAnalysis.js +1 -1
- package/build/types/ReuseOutcome.d.ts +102 -6
- package/build/types/ReuseOutcome.js +16 -2
- package/build/types/TestRecommendation.js +21 -3
- package/build/types/TestTypes.js +14 -8
- package/build/types/TestbotReport.d.ts +25 -3
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +69 -1
- package/build/utils/AnalysisStateManager.js +69 -5
- package/build/utils/branchDiff.d.ts +10 -0
- package/build/utils/branchDiff.js +28 -0
- package/build/utils/changedRoutes.d.ts +29 -0
- package/build/utils/changedRoutes.js +87 -0
- package/build/utils/featureFlags.d.ts +21 -0
- package/build/utils/featureFlags.js +23 -0
- package/build/utils/frontendIntegration.js +34 -4
- package/build/utils/importerHop.d.ts +2 -8
- package/build/utils/importerHop.js +15 -53
- package/build/utils/pathMatching.d.ts +38 -0
- package/build/utils/pathMatching.js +71 -0
- package/build/utils/pathSignatures.d.ts +22 -0
- package/build/utils/pathSignatures.js +57 -0
- package/build/utils/planMatchKeys.d.ts +16 -3
- package/build/utils/planMatchKeys.js +26 -10
- package/build/utils/pluralization.d.ts +10 -0
- package/build/utils/pluralization.js +18 -0
- package/build/utils/pom-catalog-parse.d.ts +52 -0
- package/build/utils/pom-catalog-parse.js +141 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
- package/build/utils/pom-scope/selector-extractor.js +34 -8
- package/build/utils/pom-verify/verify.d.ts +6 -5
- package/build/utils/pom-verify/verify.js +8 -6
- package/build/utils/reportLanguage.d.ts +43 -0
- package/build/utils/reportLanguage.js +125 -0
- package/build/utils/reportVerification.d.ts +74 -4
- package/build/utils/reportVerification.js +259 -3
- package/build/utils/reuseRouting.d.ts +3 -0
- package/build/utils/reuseRouting.js +50 -0
- package/build/utils/routeParsers.d.ts +2 -0
- package/build/utils/routeParsers.js +65 -8
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +57 -45
- package/build/utils/subjectEndpoints.d.ts +19 -0
- package/build/utils/subjectEndpoints.js +98 -0
- package/build/utils/testFileClassification.d.ts +11 -0
- package/build/utils/testFileClassification.js +47 -0
- package/build/utils/uiPageEnumerator.d.ts +45 -19
- package/build/utils/uiPageEnumerator.js +95 -51
- package/build/utils/utils-verify/allow.d.ts +16 -0
- package/build/utils/utils-verify/allow.js +68 -0
- package/build/utils/utils-verify/call-sites.d.ts +34 -0
- package/build/utils/utils-verify/call-sites.js +154 -0
- package/build/utils/utils-verify/index.d.ts +7 -0
- package/build/utils/utils-verify/index.js +7 -0
- package/build/utils/utils-verify/language-spec.d.ts +91 -0
- package/build/utils/utils-verify/language-spec.js +210 -0
- package/build/utils/utils-verify/locate.d.ts +39 -0
- package/build/utils/utils-verify/locate.js +199 -0
- package/build/utils/utils-verify/parse.d.ts +34 -0
- package/build/utils/utils-verify/parse.js +177 -0
- package/build/utils/utils-verify/stage.d.ts +24 -0
- package/build/utils/utils-verify/stage.js +107 -0
- package/build/utils/utils-verify/verify.d.ts +63 -0
- package/build/utils/utils-verify/verify.js +168 -0
- package/build/utils/utils.d.ts +3 -1
- package/build/utils/utils.js +3 -1
- package/build/workspace/workspace.d.ts +32 -32
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
- 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.VEfqi1qN.js +5 -0
- 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/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
|
@@ -10,6 +10,10 @@ import { SCENARIO_CATEGORIES, CATEGORY_PRIORITY, Novelty, PriorityTier } from ".
|
|
|
10
10
|
import { HttpMethod, TestType } from "../../types/TestTypes.js";
|
|
11
11
|
import { CandidateSource, computeCandidateId, scenarioMergeKey, DiscriminatorKind } from "../../types/Recommendation.js";
|
|
12
12
|
import { selectPlan } from "../../recommendation/planRanker.js";
|
|
13
|
+
import { resolveSubjectEndpoints } from "../../utils/subjectEndpoints.js";
|
|
14
|
+
import { collectChangedRouteLines } from "../../utils/changedRoutes.js";
|
|
15
|
+
import { reservedUISlots } from "../../recommendation/budgeters/shared.js";
|
|
16
|
+
import { inferScenarioType } from "../../recommendation/diversity.js";
|
|
13
17
|
import { validateDiscriminator } from "../../recommendation/discriminators.js";
|
|
14
18
|
import { isAttackSurfaceSecurityBoundary } from "../../prompts/test-recommendation/recommendationShared.js";
|
|
15
19
|
import { MAX_RECOMMENDATIONS, MAX_TESTS_TO_GENERATE } from "../../prompts/test-recommendation/recommendationSections.js";
|
|
@@ -47,7 +51,10 @@ const registerStepSchema = z.object({
|
|
|
47
51
|
.record(z.any())
|
|
48
52
|
.optional()
|
|
49
53
|
.describe("Query parameters with concrete values. Also scanned for boundary_equality and negative_match verification."),
|
|
50
|
-
responseBody: z
|
|
54
|
+
responseBody: z
|
|
55
|
+
.union([z.record(z.any()), z.array(z.any())])
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Expected response body shape/values, when asserted. An array for a collection/list endpoint that returns a JSON array."),
|
|
51
58
|
expectedStatusCode: z.number().int().describe("HTTP status code this step asserts (e.g. 201, 400, 404)."),
|
|
52
59
|
expectedResponseFields: z
|
|
53
60
|
.array(z.string())
|
|
@@ -84,10 +91,12 @@ const registerTestPlanSchema = {
|
|
|
84
91
|
.string()
|
|
85
92
|
.refine((p) => path.isAbsolute(p), { message: "stateFile must be an absolute path" })
|
|
86
93
|
.describe("Path to state file from skyramp_analyze_changes"),
|
|
87
|
-
repository
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
// No `repository` argument, deliberately (SKYR-4204 reopen). The approved
|
|
95
|
+
// plan is run-wide — one pooled registration covers every repository in the
|
|
96
|
+
// run — so unlike the per-repo tools (analyze_test_health, actions,
|
|
97
|
+
// execute_test) there is no section to select: the tool reads the root and
|
|
98
|
+
// derives run-wide facts (diff union, budget flags, scenario lookup) itself.
|
|
99
|
+
// A legacy caller still passing `repository` has it stripped harmlessly.
|
|
91
100
|
candidates: z
|
|
92
101
|
.array(registerCandidateSchema)
|
|
93
102
|
.default([])
|
|
@@ -115,8 +124,8 @@ function toScenarioStep(step) {
|
|
|
115
124
|
chainsFrom: step.chainsFrom,
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
|
-
function toDraftedScenario(input) {
|
|
119
|
-
|
|
127
|
+
function toDraftedScenario(input, changedRoutes) {
|
|
128
|
+
const scenario = {
|
|
120
129
|
scenarioName: input.scenarioName,
|
|
121
130
|
description: input.description,
|
|
122
131
|
category: input.category,
|
|
@@ -127,13 +136,20 @@ function toDraftedScenario(input) {
|
|
|
127
136
|
estimatedComplexity: "moderate",
|
|
128
137
|
testType: input.testType,
|
|
129
138
|
};
|
|
139
|
+
// Resolved here, at the one boundary that has both the steps and the diff, so
|
|
140
|
+
// every later dedup stage reads one recorded value (SKYR-4214).
|
|
141
|
+
return { ...scenario, subjectEndpoints: resolveSubjectEndpoints(scenario, { changedRoutes }) };
|
|
130
142
|
}
|
|
131
143
|
/** Build the agent-submitted candidates. Discriminator claims are verified
|
|
132
144
|
* later, by {@link applyDiscriminatorClaims}, once the merge has settled which
|
|
133
|
-
* scenario each name resolves to.
|
|
134
|
-
|
|
145
|
+
* scenario each name resolves to. Exported for the direct unit test in
|
|
146
|
+
* registerTestPlanTool.test.ts — a candidate that does not survive selection
|
|
147
|
+
* never reaches the persisted plan, so its subjectEndpoints fill is not
|
|
148
|
+
* observable through the tool's output, and testing here avoids coupling the
|
|
149
|
+
* fill to what selection happens to keep. */
|
|
150
|
+
export function buildAgentCandidates(candidates, changedRoutes) {
|
|
135
151
|
return candidates.map((input) => {
|
|
136
|
-
const scenario = toDraftedScenario(input);
|
|
152
|
+
const scenario = toDraftedScenario(input, changedRoutes);
|
|
137
153
|
return {
|
|
138
154
|
scenario,
|
|
139
155
|
priority: derivePriorityTier(scenario),
|
|
@@ -226,6 +242,13 @@ function resolveBudgetContext(stateData, fullState) {
|
|
|
226
242
|
isUIOnlyPR: pbc?.isUIOnlyPR ?? false,
|
|
227
243
|
hasFrontendChanges: pbc?.hasFrontendChanges ?? false,
|
|
228
244
|
externalCoverage: new Set(pbc?.externalCoverageKeys ?? []),
|
|
245
|
+
// `true` for the same reason `computeDiffChangesTestFiles` answers `true` with
|
|
246
|
+
// no diff: an unknown must shut the reserve, not open it. State written by an
|
|
247
|
+
// older MCP build carries no such field, and if that PR did add its own tests
|
|
248
|
+
// the reserve would recommend duplicates of them — the failure this flag
|
|
249
|
+
// exists to stop, reached by version skew alone. It is also the pre-reserve
|
|
250
|
+
// behaviour: covered candidates were dropped.
|
|
251
|
+
diffChangesTestFiles: pbc?.diffChangesTestFiles ?? true,
|
|
229
252
|
};
|
|
230
253
|
const relatedSections = Object.values(fullState?.relatedRepos ?? {}).map((section) => section.data);
|
|
231
254
|
if (relatedSections.length === 0)
|
|
@@ -245,12 +268,23 @@ function resolveBudgetContext(stateData, fullState) {
|
|
|
245
268
|
const hasFrontendChanges = sections.some((s) => s?.planBudgetContext?.hasFrontendChanges ?? false);
|
|
246
269
|
const hasApiChanges = sections.some(sectionHasApiChanges);
|
|
247
270
|
const rootPbc = fullState?.planBudgetContext;
|
|
271
|
+
// SKYR-4211: external-test coverage is also a per-section fact — a backend
|
|
272
|
+
// candidate already covered by an external test recorded in a RELATED repo's
|
|
273
|
+
// section must dedup the same as one covered in the primary. Union the keys
|
|
274
|
+
// across all sections, like the flags above.
|
|
275
|
+
const externalCoverage = new Set(sections.flatMap((s) => s?.planBudgetContext?.externalCoverageKeys ?? []));
|
|
248
276
|
return {
|
|
249
277
|
maxGenerate: rootPbc?.maxGenerate ?? base.maxGenerate,
|
|
250
278
|
maxTotal: rootPbc?.maxTotal ?? base.maxTotal,
|
|
251
279
|
isUIOnlyPR: hasFrontendChanges && !hasApiChanges,
|
|
252
280
|
hasFrontendChanges,
|
|
253
|
-
externalCoverage
|
|
281
|
+
externalCoverage,
|
|
282
|
+
// Unioned across sections like the flags above: a pull request that adds
|
|
283
|
+
// its own tests in ANY repo of the run must shut the reserve, or that repo's
|
|
284
|
+
// own coverage becomes grounds to recommend duplicates of it. A section with
|
|
285
|
+
// no budget context is an unknown, and an unknown shuts the reserve — the
|
|
286
|
+
// same direction `computeDiffChangesTestFiles` takes with no diff.
|
|
287
|
+
diffChangesTestFiles: sections.some((s) => s?.planBudgetContext?.diffChangesTestFiles ?? true),
|
|
254
288
|
};
|
|
255
289
|
}
|
|
256
290
|
function describeGenerationCall(item) {
|
|
@@ -306,7 +340,7 @@ function renderGenerationDirective(plan) {
|
|
|
306
340
|
(nonUICount > 0 ? ` Generate the ${tests(nonUICount)} of other types as well.` : ""),
|
|
307
341
|
];
|
|
308
342
|
}
|
|
309
|
-
function renderPlanText(plan) {
|
|
343
|
+
function renderPlanText(plan, dropped) {
|
|
310
344
|
const lines = [];
|
|
311
345
|
lines.push(`## Approved Test Plan (${plan.planId})`);
|
|
312
346
|
lines.push("");
|
|
@@ -329,10 +363,41 @@ function renderPlanText(plan) {
|
|
|
329
363
|
plan.additional.forEach((item, i) => lines.push(renderPlanItem(item, plan.generate.length + i + 1)));
|
|
330
364
|
}
|
|
331
365
|
if (plan.demotions.length > 0) {
|
|
366
|
+
// A demotion (failed discriminator claim) and a drop (removed by the
|
|
367
|
+
// budgeter for an unrelated reason — coverage or budget) are independent:
|
|
368
|
+
// a candidate can suffer both. Split so the report never claims a
|
|
369
|
+
// dropped candidate is "still registered".
|
|
370
|
+
const dropReasonByCandidateId = new Map(dropped.map((d) => [d.candidateId, d.reason]));
|
|
371
|
+
const registeredDemotions = plan.demotions.filter((d) => !dropReasonByCandidateId.has(d.candidateId));
|
|
372
|
+
const removedDemotions = plan.demotions.filter((d) => dropReasonByCandidateId.has(d.candidateId));
|
|
373
|
+
if (registeredDemotions.length > 0) {
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push(`### Demotions (${registeredDemotions.length}) — discriminator claims that did NOT verify (candidate still registered, claim dropped)`);
|
|
376
|
+
for (const demotion of registeredDemotions) {
|
|
377
|
+
lines.push(`- ${demotion.candidateId}: ${demotion.reason}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (removedDemotions.length > 0) {
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push(`### Demoted AND removed (${removedDemotions.length}) — the claim failed to verify AND the candidate left the plan. It is NOT registered.`);
|
|
383
|
+
for (const demotion of removedDemotions) {
|
|
384
|
+
lines.push(`- ${demotion.candidateId}: claim failed — ${demotion.reason}; removed — ${dropReasonByCandidateId.get(demotion.candidateId)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Every other removal — budget or coverage, with no failed claim attached.
|
|
389
|
+
// This section is independent of `demotions`: the two channels are unrelated,
|
|
390
|
+
// and the motivating case has drops and NO demotions. Nesting it inside the
|
|
391
|
+
// demotion block returned empty GENERATE/ADDITIONAL lists with no candidate id
|
|
392
|
+
// and no reason, which is the state item 5 exists to make visible. Measured on
|
|
393
|
+
// eval run 32586610615, fixture repo15-P14: 10 drops, 0 demotions.
|
|
394
|
+
const demotedIds = new Set(plan.demotions.map((d) => d.candidateId));
|
|
395
|
+
const removedWithoutDemotion = dropped.filter((d) => !demotedIds.has(d.candidateId));
|
|
396
|
+
if (removedWithoutDemotion.length > 0) {
|
|
332
397
|
lines.push("");
|
|
333
|
-
lines.push(`###
|
|
334
|
-
for (const
|
|
335
|
-
lines.push(`- ${
|
|
398
|
+
lines.push(`### Removed (${removedWithoutDemotion.length}) — candidates the selection stage dropped. They are NOT registered.`);
|
|
399
|
+
for (const drop of removedWithoutDemotion) {
|
|
400
|
+
lines.push(`- ${drop.candidateId}: ${drop.reason}`);
|
|
336
401
|
}
|
|
337
402
|
}
|
|
338
403
|
lines.push("");
|
|
@@ -359,12 +424,16 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
359
424
|
}, async (params) => {
|
|
360
425
|
let errorResult;
|
|
361
426
|
try {
|
|
362
|
-
logger.info(
|
|
427
|
+
logger.info("Registering run-wide test plan", {
|
|
363
428
|
candidateCount: params.candidates?.length ?? 0,
|
|
364
429
|
});
|
|
365
430
|
const stateManager = StateManager.fromStatePath(params.stateFile);
|
|
366
|
-
|
|
367
|
-
|
|
431
|
+
// The plan is run-wide, so register always reads the ROOT (primary)
|
|
432
|
+
// section — there is no section selector (routing by one is what let a
|
|
433
|
+
// related-repo registration run with that section's context —
|
|
434
|
+
// SKYR-4204 reopen). Run-wide facts (diff union, budget flags,
|
|
435
|
+
// scenario lookup) come from fullState below.
|
|
436
|
+
const stateData = await stateManager.readData();
|
|
368
437
|
if (!stateData) {
|
|
369
438
|
errorResult = toolError(`State file is empty or invalid: ${params.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
370
439
|
return errorResult;
|
|
@@ -388,9 +457,20 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
388
457
|
];
|
|
389
458
|
const diffText = diffSections.map((s) => s.diff).filter(Boolean).join("\n");
|
|
390
459
|
const reposWithoutDiff = diffSections.filter((s) => !s.diff).map((s) => s.repo);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
460
|
+
// Scenario lookup for server-candidate recovery spans ALL sections —
|
|
461
|
+
// the primary's drafted scenarios must resolve even when `repository`
|
|
462
|
+
// targets a related repo (SKYR-4204 reopen: the same single-section
|
|
463
|
+
// shape as the diff union above).
|
|
464
|
+
const allScenarios = [
|
|
465
|
+
...(fullState?.repositoryAnalysis?.scenarios ?? []),
|
|
466
|
+
...Object.values(fullState?.relatedRepos ?? {}).flatMap((section) => section.data?.repositoryAnalysis?.scenarios ?? []),
|
|
467
|
+
];
|
|
468
|
+
const changedRoutes = collectChangedRouteLines(diffText);
|
|
469
|
+
const agentCandidates = buildAgentCandidates(params.candidates ?? [], changedRoutes);
|
|
470
|
+
// The approved plan is run-wide and persists at the ROOT (see the
|
|
471
|
+
// persist step below), so prior-plan recovery reads the root first;
|
|
472
|
+
// the section fallback covers state written by older builds.
|
|
473
|
+
const serverCandidates = recoverServerCandidates(fullState?.approvedPlan ?? stateData.approvedPlan, allScenarios);
|
|
394
474
|
// Merge by scenario-name identity, NOT the full content-hashed candidateId:
|
|
395
475
|
// the server (analyze_changes) and the agent frequently draft their own
|
|
396
476
|
// independent steps[] for "the same" scenario (identical scenarioName),
|
|
@@ -433,7 +513,44 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
433
513
|
"Submit your complete candidate list (every test you would generate or recommend) in `candidates` — an empty plan would block all generation.");
|
|
434
514
|
return errorResult;
|
|
435
515
|
}
|
|
516
|
+
// Review (mcp#774): enforce ONE run-wide plan at the tool boundary. A
|
|
517
|
+
// re-registration whose pool shares no scenario with the active plan
|
|
518
|
+
// is the per-repo-partial signature (the demoshop rig's second,
|
|
519
|
+
// backend-only call) — accepting it would silently REPLACE the run's
|
|
520
|
+
// plan with a subset and drop every earlier agent candidate. Genuine
|
|
521
|
+
// full-list updates overlap the active plan and replace it as before.
|
|
522
|
+
// The overlap test spans GENERATE ∪ ADDITIONAL: with maxGenerate=0 the
|
|
523
|
+
// active plan's GENERATE is empty while ADDITIONAL carries every
|
|
524
|
+
// candidate, and a disjoint partial could otherwise still replace it
|
|
525
|
+
// silently (Copilot review on #774).
|
|
526
|
+
const priorItems = [
|
|
527
|
+
...(fullState?.approvedPlan?.generate ?? []),
|
|
528
|
+
...(fullState?.approvedPlan?.additional ?? []),
|
|
529
|
+
];
|
|
530
|
+
if (priorItems.length > 0) {
|
|
531
|
+
const poolKeys = new Set(allCandidates.map(mergeKey));
|
|
532
|
+
const overlaps = priorItems.some((item) => poolKeys.has(scenarioMergeKey(item.scenarioName) || item.candidateId));
|
|
533
|
+
if (!overlaps) {
|
|
534
|
+
errorResult = toolError(`A run-wide plan is already active (${fullState.approvedPlan.planId}: ` +
|
|
535
|
+
`${priorItems.map((i) => i.scenarioName).join(", ")}) and this submission shares none of its ` +
|
|
536
|
+
"scenarios. skyramp_register_test_plan REPLACES the whole run's plan — resubmit ONE pooled candidate " +
|
|
537
|
+
"list for the entire run (your previous candidates plus these changes), not a per-repo subset.");
|
|
538
|
+
return errorResult;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
436
541
|
const budgetContext = resolveBudgetContext(stateData, fullState);
|
|
542
|
+
// SKYR-4204 reopen: a reserved UI slot is only fillable from the
|
|
543
|
+
// candidate pool. A pool with no UI candidate on a run with frontend
|
|
544
|
+
// changes would silently plan zero UI tests (runs 32419025266,
|
|
545
|
+
// 32431042133, 32431106864) — reject so the agent self-corrects in one
|
|
546
|
+
// step, exactly like the empty-candidates rejection above.
|
|
547
|
+
if (reservedUISlots(budgetContext) > 0 &&
|
|
548
|
+
!allCandidates.some((c) => inferScenarioType(c.scenario) === TestType.UI)) {
|
|
549
|
+
errorResult = toolError("This run has frontend changes, so the plan reserves a UI slot — but no submitted or recovered " +
|
|
550
|
+
"candidate has testType 'ui'. Resubmit your full candidate list including at least one UI candidate " +
|
|
551
|
+
"for the changed frontend (see uiContext.changedFrontendFiles).");
|
|
552
|
+
return errorResult;
|
|
553
|
+
}
|
|
437
554
|
const result = selectPlan(allCandidates, { ...budgetContext, demotions, diffText });
|
|
438
555
|
const approvedPlan = {
|
|
439
556
|
planId: crypto.randomUUID(),
|
|
@@ -441,16 +558,25 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
441
558
|
generate: result.generate.map(buildApprovedPlanItem),
|
|
442
559
|
additional: result.additional.map(buildApprovedPlanItem),
|
|
443
560
|
demotions: result.demotions,
|
|
561
|
+
dropped: result.dropped,
|
|
444
562
|
};
|
|
563
|
+
// The approved plan is the ONE run-wide authority (the prompt mandates
|
|
564
|
+
// a single pooled registration on multi-repo runs), so it persists at
|
|
565
|
+
// the state-file ROOT regardless of `repository`. Per-section plans
|
|
566
|
+
// blinded every root-reading consumer — planGuard let generation run
|
|
567
|
+
// unguarded, submitReportTool's reconciliation was inert, and the eval
|
|
568
|
+
// extractor saw "no plan" (SKYR-4204 reopen). writeData preserves the
|
|
569
|
+
// relatedRepos sections and the root metadata.
|
|
445
570
|
try {
|
|
446
|
-
|
|
571
|
+
const { metadata: _meta, relatedRepos: _sections, ...rootData } = fullState ?? {};
|
|
572
|
+
await stateManager.writeData({ ...rootData, approvedPlan }, { step: TOOL_NAME });
|
|
447
573
|
}
|
|
448
574
|
catch (error) {
|
|
449
575
|
errorResult = toolError(`Failed to persist approved test plan to state file: ${error.message}. Retry this call.`);
|
|
450
576
|
return errorResult;
|
|
451
577
|
}
|
|
452
578
|
return {
|
|
453
|
-
content: [{ type: "text", text: renderPlanText(approvedPlan) }],
|
|
579
|
+
content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped) }],
|
|
454
580
|
};
|
|
455
581
|
}
|
|
456
582
|
catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DraftedScenario } from "../types/RepositoryAnalysis.js";
|
|
1
|
+
import { DraftedScenario, SubjectEndpoint } from "../types/RepositoryAnalysis.js";
|
|
2
2
|
import { Novelty, PriorityTier } from "../types/TestRecommendation.js";
|
|
3
3
|
/**
|
|
4
4
|
* The structural bug-shape a candidate claims to probe. Verified against the
|
|
@@ -53,21 +53,42 @@ export interface BudgetContext {
|
|
|
53
53
|
hasFrontendChanges: boolean;
|
|
54
54
|
/** Method-aware coverage keys of external tests; matching candidates are dropped. */
|
|
55
55
|
externalCoverage: Set<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the PR's own diff changes any test file. External coverage is
|
|
58
|
+
* discovered from the working tree, so a test the PR itself adds is
|
|
59
|
+
* indistinguishable from pre-existing coverage; when this is true the
|
|
60
|
+
* covered-candidate reserve stays shut rather than forcing duplicates of a
|
|
61
|
+
* test the author already wrote.
|
|
62
|
+
*/
|
|
63
|
+
diffChangesTestFiles: boolean;
|
|
56
64
|
}
|
|
57
65
|
/** A demoted claim: which candidate, and the human-readable reason returned to
|
|
58
|
-
* the agent so it can fix the claim (never a silent drop).
|
|
66
|
+
* the agent so it can fix the claim (never a silent drop of the claim itself).
|
|
67
|
+
* A demotion records that a declared discriminator claim failed to verify.
|
|
68
|
+
* It does not say whether the candidate is still in the plan. Budgeting
|
|
69
|
+
* decides that separately. A candidate can be both demoted and dropped. */
|
|
59
70
|
export interface Demotion {
|
|
60
71
|
candidateId: string;
|
|
61
72
|
reason: string;
|
|
62
73
|
}
|
|
74
|
+
/** A candidate the selection stage removed, and why. A demotion annotates a
|
|
75
|
+
* claim. A drop removes a candidate from `generate` and `additional`. The
|
|
76
|
+
* two are independent. The same candidate can carry both. */
|
|
77
|
+
export interface Dropped {
|
|
78
|
+
candidateId: string;
|
|
79
|
+
reason: string;
|
|
80
|
+
}
|
|
63
81
|
/** Result of budgeting: the GENERATE set, the ADDITIONAL set, how many UI
|
|
64
|
-
* placeholder slots the render layer should add (mixed/UI-only PRs),
|
|
65
|
-
* demoted-claim channel (the missing rejection surface for register-plan)
|
|
82
|
+
* placeholder slots the render layer should add (mixed/UI-only PRs), the
|
|
83
|
+
* demoted-claim channel (the missing rejection surface for register-plan),
|
|
84
|
+
* and the dropped-candidate channel (SKYR-4214: every candidate the
|
|
85
|
+
* selection stage removed outright, with a human-actionable reason). */
|
|
66
86
|
export interface SelectionResult {
|
|
67
87
|
generate: Candidate[];
|
|
68
88
|
additional: Candidate[];
|
|
69
89
|
reservedUISlots: number;
|
|
70
90
|
demotions: Demotion[];
|
|
91
|
+
dropped: Dropped[];
|
|
71
92
|
}
|
|
72
93
|
/** A pluggable budgeting algorithm. Deterministic and pure. */
|
|
73
94
|
export interface Budgeter {
|
|
@@ -105,7 +126,14 @@ export declare function scenarioMergeKey(scenarioName: string | undefined): stri
|
|
|
105
126
|
export declare function computeCandidateId(scenario: DraftedScenario): string;
|
|
106
127
|
/** One approved plan item as persisted in `UnifiedAnalysisState.approvedPlan`.
|
|
107
128
|
* Deliberately slim (no steps/requestBody/etc.) — `matchKeys` already carries
|
|
108
|
-
* everything a downstream matcher needs.
|
|
129
|
+
* everything a downstream matcher needs.
|
|
130
|
+
*
|
|
131
|
+
* `subjectEndpoints` is the one exception. `matchKeys` holds one key per
|
|
132
|
+
* step and is deliberately broad, so a generated test can be matched back
|
|
133
|
+
* to its plan item. It does not say which step is the subject. Dedup
|
|
134
|
+
* judges the subject, not the whole step list, so the plan item must
|
|
135
|
+
* record it too. See `DraftedScenario.subjectEndpoints` for the
|
|
136
|
+
* absent-vs-empty contract this field carries over unchanged. */
|
|
109
137
|
export interface ApprovedPlanItem {
|
|
110
138
|
candidateId: string;
|
|
111
139
|
scenarioName: string;
|
|
@@ -113,5 +141,6 @@ export interface ApprovedPlanItem {
|
|
|
113
141
|
category: string;
|
|
114
142
|
source: CandidateSource;
|
|
115
143
|
verifiedDiscriminator?: string;
|
|
144
|
+
subjectEndpoints?: SubjectEndpoint[];
|
|
116
145
|
matchKeys: string[];
|
|
117
146
|
}
|