@skyramp/mcp 0.3.7 → 0.3.9-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 +1 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +22 -1
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +4 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +41 -9
- package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
- package/build/prompts/test-recommendation/recommendationSections.js +3 -3
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
- package/build/prompts/testbot/testbot-prompts.js +35 -15
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
- package/build/recommendation/budgeters/shared.d.ts +21 -8
- package/build/recommendation/budgeters/shared.js +134 -56
- package/build/recommendation/planRanker.d.ts +5 -3
- package/build/recommendation/planRanker.js +1 -1
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/submitReportTool.d.ts +5 -5
- package/build/tools/submitReportTool.js +11 -2
- package/build/tools/test-management/actionsTool.js +71 -3
- package/build/tools/test-management/analyzeChangesTool.d.ts +10 -0
- package/build/tools/test-management/analyzeChangesTool.js +73 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +29 -8
- package/build/types/RepositoryAnalysis.d.ts +10 -10
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +11 -1
- package/build/types/TestRecommendation.js +34 -0
- package/build/utils/AnalysisStateManager.d.ts +7 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/frontendSelectors.d.ts +33 -0
- package/build/utils/frontendSelectors.js +196 -5
- package/build/utils/pathSignatures.d.ts +4 -1
- package/build/utils/pathSignatures.js +14 -2
- package/build/utils/planOnlyMode.d.ts +33 -0
- package/build/utils/planOnlyMode.js +40 -0
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +34 -0
- package/build/utils/removedUiElements.js +153 -0
- package/build/utils/reportVerification.d.ts +7 -6
- package/build/utils/reportVerification.js +68 -4
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +1 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- 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/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
|
@@ -8,7 +8,8 @@ import * as path from "path";
|
|
|
8
8
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
9
9
|
import { dualChannelResult, toolError } from "../../utils/utils.js";
|
|
10
10
|
import { isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
11
|
-
import {
|
|
11
|
+
import { REBASELINE_SNAPSHOT_NAME_RE, looksLikeOnDiskBaselineName } from "../../utils/rebaselineSnapshots.js";
|
|
12
|
+
import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRebaselineStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
|
|
12
13
|
/**
|
|
13
14
|
* Compute a suggested new filename when an endpoint is renamed.
|
|
14
15
|
*/
|
|
@@ -102,6 +103,15 @@ const recommendationSchema = z.object({
|
|
|
102
103
|
.optional()
|
|
103
104
|
.describe("For a large UPDATE file, the concrete per-site edits you already located while reading it — one entry per place that must change, with a verbatim anchor. " +
|
|
104
105
|
"Surfaced to the apply step as a checklist so it edits each site directly instead of re-grepping/re-paging the file. Provide this whenever an UPDATE touches more than a couple of sites."),
|
|
106
|
+
rebaselineSnapshots: z
|
|
107
|
+
// Loose here on purpose: one malformed name must not reject the whole batch and
|
|
108
|
+
// lose every other verdict. Entries are validated per item below; invalid ones
|
|
109
|
+
// are dropped and surfaced in the response.
|
|
110
|
+
.array(z.string())
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("UPDATE only — the toHaveScreenshot() baseline filenames (e.g. [\"page-001.png\"]) whose captured page/element/region the diff visibly changed, so the committed PNG is stale (SKYR-4298). " +
|
|
113
|
+
"List only the baselines the change reaches; leave unaffected ones out. These are refreshed at the final skyramp_execute_test run via its rebaselineSnapshots parameter, never by editing the assertion, its maxDiffPixelRatio, or the PNG. " +
|
|
114
|
+
"Omit when the diff does not explain a screenshot mismatch — that failure stays red."),
|
|
105
115
|
renamedEndpoints: z
|
|
106
116
|
.array(z.object({
|
|
107
117
|
oldPath: z.string().describe("Previous endpoint path"),
|
|
@@ -182,6 +192,7 @@ export function registerActionsTool(server) {
|
|
|
182
192
|
// and passes results here directly — analyzeTestHealthTool never writes assessment
|
|
183
193
|
// data back to the state file.
|
|
184
194
|
const recommendations = [];
|
|
195
|
+
const droppedRebaselineNotes = [];
|
|
185
196
|
(args.recommendations ?? []).forEach((rec) => {
|
|
186
197
|
// Schema requires absolute paths; resolve any relative paths defensively
|
|
187
198
|
// against repoRoot in case the LLM sends a relative path despite the schema.
|
|
@@ -294,6 +305,26 @@ export function registerActionsTool(server) {
|
|
|
294
305
|
resolvedPomFile = undefined;
|
|
295
306
|
}
|
|
296
307
|
}
|
|
308
|
+
if (rec.action !== DriftAction.Update && rec.rebaselineSnapshots?.length) {
|
|
309
|
+
droppedRebaselineNotes.push(`${rec.testFilePath}: rebaselineSnapshots [${rec.rebaselineSnapshots.join(", ")}] ignored on ${rec.action} — a baseline refresh is only meaningful on UPDATE.`);
|
|
310
|
+
}
|
|
311
|
+
// Per-entry validation (the schema is deliberately loose, see above).
|
|
312
|
+
const validBaselines = [];
|
|
313
|
+
for (const raw of rec.rebaselineSnapshots ?? []) {
|
|
314
|
+
const name = raw.trim();
|
|
315
|
+
if (!REBASELINE_SNAPSHOT_NAME_RE.test(name)) {
|
|
316
|
+
droppedRebaselineNotes.push(`${rec.testFilePath}: rebaselineSnapshots entry ${JSON.stringify(raw)} dropped — must be the bare .png name the test passes to toHaveScreenshot(), e.g. "page-001.png" (no path, commas, or whitespace).`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (looksLikeOnDiskBaselineName(name)) {
|
|
320
|
+
// Kept — a test can legitimately pass a name like this — but flagged: the
|
|
321
|
+
// on-disk file carries Playwright's project/platform suffix, and passing it
|
|
322
|
+
// by mistake refreshes nothing (the execution result will say NOT refreshed).
|
|
323
|
+
droppedRebaselineNotes.push(`${rec.testFilePath}: rebaselineSnapshots entry "${name}" kept but looks like an on-disk filename (Playwright's project/platform suffix). If the test passes "${name.replace(/-(?:[^-]+-)?(?:darwin|linux|win32)\.png$/i, ".png")}" to toHaveScreenshot(), pass that instead.`);
|
|
324
|
+
}
|
|
325
|
+
if (!validBaselines.includes(name))
|
|
326
|
+
validBaselines.push(name);
|
|
327
|
+
}
|
|
297
328
|
recommendations.push({
|
|
298
329
|
testFilePath: resolvedFile,
|
|
299
330
|
pomFile: resolvedPomFile,
|
|
@@ -306,6 +337,19 @@ export function registerActionsTool(server) {
|
|
|
306
337
|
updateInstructions: rec.updateInstructions ?? "",
|
|
307
338
|
renamedEndpoints: rec.renamedEndpoints ?? [],
|
|
308
339
|
...(rec.editSites?.length ? { editSites: rec.editSites } : {}),
|
|
340
|
+
// Only an UPDATE refreshes a baseline. A REGENERATEd spec that reuses a
|
|
341
|
+
// baseline name still compares against the committed PNG, and DELETE
|
|
342
|
+
// removes the spec — neither is a refresh, so the list is dropped (and
|
|
343
|
+
// surfaced in the response below rather than silently discarded).
|
|
344
|
+
...(rec.action === DriftAction.Update && validBaselines.length
|
|
345
|
+
? {
|
|
346
|
+
rebaselineSnapshots: validBaselines,
|
|
347
|
+
// No updateInstructions → the refresh is the whole maintenance and
|
|
348
|
+
// the report gate must not expect a spec/POM edit. With
|
|
349
|
+
// updateInstructions the gate holds the UPDATE to both.
|
|
350
|
+
rebaselineOnly: !rec.updateInstructions,
|
|
351
|
+
}
|
|
352
|
+
: {}),
|
|
309
353
|
});
|
|
310
354
|
});
|
|
311
355
|
// Derive the no-action set here instead of asking the LLM to enumerate it.
|
|
@@ -340,6 +384,11 @@ export function registerActionsTool(server) {
|
|
|
340
384
|
rationale: r.rationale,
|
|
341
385
|
testType: r.testType,
|
|
342
386
|
endpoint: r.endpoint,
|
|
387
|
+
// Persisted so the report-time working-tree check knows this UPDATE's
|
|
388
|
+
// change is the refreshed PNG, not the spec (SKYR-4298).
|
|
389
|
+
...(r.rebaselineSnapshots?.length
|
|
390
|
+
? { rebaselineSnapshots: r.rebaselineSnapshots, rebaselineOnly: r.rebaselineOnly === true }
|
|
391
|
+
: {}),
|
|
343
392
|
})),
|
|
344
393
|
}, { repo: args.repository, repositoryPath, step: "actions" });
|
|
345
394
|
}
|
|
@@ -449,6 +498,7 @@ export function registerActionsTool(server) {
|
|
|
449
498
|
suggestedNewFile,
|
|
450
499
|
updateInstructions: rec.updateInstructions,
|
|
451
500
|
rationale: rec.rationale,
|
|
501
|
+
rebaselineSnapshots: rec.rebaselineSnapshots,
|
|
452
502
|
}));
|
|
453
503
|
}
|
|
454
504
|
// ── REGENERATE: emit overwrite instructions ──
|
|
@@ -500,6 +550,10 @@ export function registerActionsTool(server) {
|
|
|
500
550
|
deleteRecommendations.length === 0) {
|
|
501
551
|
sections.push(`No action required. All existing tests appear healthy.`);
|
|
502
552
|
}
|
|
553
|
+
if (droppedRebaselineNotes.length > 0) {
|
|
554
|
+
sections.push(`## rebaselineSnapshots notes (${droppedRebaselineNotes.length})\n\n` +
|
|
555
|
+
droppedRebaselineNotes.map((n) => `- ${n}`).join("\n"));
|
|
556
|
+
}
|
|
503
557
|
sections.push(`**This tool is currently in Early Preview stage. Please verify the results.**`);
|
|
504
558
|
const responseText = sections.join("\n\n");
|
|
505
559
|
// ── Build LLM instructions ──
|
|
@@ -534,18 +588,32 @@ export function registerActionsTool(server) {
|
|
|
534
588
|
// Update context: per-file guidance + current content for the downstream LLM.
|
|
535
589
|
// Including file content avoids re-reads on each Edit turn, reducing token usage.
|
|
536
590
|
const updateContext = updateRecommendations
|
|
537
|
-
.filter((rec) => !!rec.updateInstructions)
|
|
591
|
+
.filter((rec) => !!rec.updateInstructions || !!rec.rebaselineSnapshots?.length)
|
|
538
592
|
.map((rec) => {
|
|
539
593
|
const editTarget = rec.pomFile ?? rec.testFilePath;
|
|
594
|
+
const hasEdit = !!rec.updateInstructions;
|
|
540
595
|
return {
|
|
541
596
|
file: editTarget,
|
|
542
|
-
|
|
597
|
+
// A rebaseline-only entry has nothing to edit: no context, and an
|
|
598
|
+
// explicit edit_required: false so the strategy's "make the changes
|
|
599
|
+
// described in context" cannot apply to it (rationale is always set,
|
|
600
|
+
// so it must not stand in for edit instructions here).
|
|
601
|
+
...(hasEdit ? { context: rec.updateInstructions } : { edit_required: false }),
|
|
543
602
|
...(rec.editSites?.length ? { edit_sites: rec.editSites } : {}),
|
|
603
|
+
// The baselines to pass as skyramp_execute_test's rebaselineSnapshots for
|
|
604
|
+
// the SPEC (testFilePath, which is what runs) — never `file`, which is
|
|
605
|
+
// the page object for a POM-backed UPDATE.
|
|
606
|
+
...(rec.rebaselineSnapshots?.length
|
|
607
|
+
? { spec_file: rec.testFilePath, rebaseline_snapshots: rec.rebaselineSnapshots }
|
|
608
|
+
: {}),
|
|
544
609
|
};
|
|
545
610
|
});
|
|
546
611
|
if (updateContext.length > 0) {
|
|
547
612
|
llmInstructionsObj.update_context = updateContext;
|
|
548
613
|
llmInstructionsObj.update_strategy = buildUpdateStrategy();
|
|
614
|
+
if (updateContext.some((u) => "rebaseline_snapshots" in u)) {
|
|
615
|
+
llmInstructionsObj.rebaseline_strategy = buildRebaselineStrategy();
|
|
616
|
+
}
|
|
549
617
|
}
|
|
550
618
|
// REGENERATE context: existing file content gives the generation tool the
|
|
551
619
|
// endpoint URL, auth pattern, test type, and language to replicate.
|
|
@@ -6,6 +6,7 @@ import type { FrontendFileIntegration } from "../../types/FrontendIntegration.js
|
|
|
6
6
|
import { TraceFile } from "../../types/RepositoryAnalysis.js";
|
|
7
7
|
import { BranchDiffData } from "../../utils/branchDiff.js";
|
|
8
8
|
import { ScannedEndpoint } from "../../utils/repoScanner.js";
|
|
9
|
+
import { RemovedUiElement } from "../../utils/removedUiElements.js";
|
|
9
10
|
import { TraceParseResult } from "../../utils/trace-parser.js";
|
|
10
11
|
/** Exported for testing: maps a parsed trace result to a TraceFile. */
|
|
11
12
|
export declare function buildTraceFileEntry(tracePath: string, result: TraceParseResult): TraceFile;
|
|
@@ -28,6 +29,15 @@ export declare const analyzeChangesInputSchema: {
|
|
|
28
29
|
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|
|
29
30
|
export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (no route files matched the changed files or their importers, and no frontend baseUrl to fall back to). UI recommendations will be source-grounded only.";
|
|
30
31
|
export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Instruction block for data-testids the diff removed from pages that survive
|
|
34
|
+
* (SKYR-4305). The UI analog of the `[removed]` endpoint 404 guard: one UI
|
|
35
|
+
* candidate per element, asserting the element is gone plus a positive check on a
|
|
36
|
+
* retained element of the same page. Whole-feature removals never reach here —
|
|
37
|
+
* collectRemovedUiElements drops elements whose file was deleted or whose page
|
|
38
|
+
* could not be resolved.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildRemovedElementsInstructions(elements: RemovedUiElement[]): string;
|
|
31
41
|
/**
|
|
32
42
|
* Instruction block for changed frontend files the server determined have no
|
|
33
43
|
* production importer (SKYR-3855) — dead/orphaned components. Used both to
|
|
@@ -33,6 +33,7 @@ import { detectProjectMetadata } from "../../utils/projectMetadata.js";
|
|
|
33
33
|
import { draftScenariosFromEndpoints, draftResponseShapeScenarios, capScenarios } from "../../utils/scenarioDrafting.js";
|
|
34
34
|
import { mapUnmatchedFilesToImporterEndpoints, buildHopResponseShapeInputs, deriveResourceTokensForFile, deriveResourceToken, extractChangedSymbols, sliceDiffByFile } from "../../utils/importerHop.js";
|
|
35
35
|
import { extractChangedSelectors, MAX_CHANGED_SELECTORS } from "../../utils/frontendSelectors.js";
|
|
36
|
+
import { collectRemovedUiElements, MAX_REMOVED_UI_ELEMENTS } from "../../utils/removedUiElements.js";
|
|
36
37
|
import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analysisOutputPrompt.js";
|
|
37
38
|
import { parseTraceFile, discoverTraceFiles, discoverPlaywrightZips, } from "../../utils/trace-parser.js";
|
|
38
39
|
import { TestSource } from "../../types/TestAnalysis.js";
|
|
@@ -359,6 +360,39 @@ If a candidate URL 404s or redirects unexpectedly, navigate from the frontend ba
|
|
|
359
360
|
|
|
360
361
|
If \`browser_blueprint\` fails on every candidate URL (app unreachable, all 404s), proceed and log an \`issuesFound\` info entry. Recommendations will be source-grounded; non-UI work is unaffected.`;
|
|
361
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Instruction block for data-testids the diff removed from pages that survive
|
|
365
|
+
* (SKYR-4305). The UI analog of the `[removed]` endpoint 404 guard: one UI
|
|
366
|
+
* candidate per element, asserting the element is gone plus a positive check on a
|
|
367
|
+
* retained element of the same page. Whole-feature removals never reach here —
|
|
368
|
+
* collectRemovedUiElements drops elements whose file was deleted or whose page
|
|
369
|
+
* could not be resolved.
|
|
370
|
+
*/
|
|
371
|
+
export function buildRemovedElementsInstructions(elements) {
|
|
372
|
+
const byPage = new Map();
|
|
373
|
+
for (const e of elements) {
|
|
374
|
+
for (const url of e.pageUrls) {
|
|
375
|
+
if (!byPage.has(url))
|
|
376
|
+
byPage.set(url, []);
|
|
377
|
+
byPage.get(url).push(e);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const target = (e) => e.attribute === "data-testid" ? `testId: "${e.value}"` : `attribute: "${e.attribute}", value: "${e.value}"`;
|
|
381
|
+
const pageLines = [...byPage.entries()]
|
|
382
|
+
.map(([url, els], i) => ` ${i + 1}. ${url}\n` + els.map((e) => ` - \`${e.attribute}="${e.value}"\` (removed from ${e.file}; browser_assert_hidden target: ${target(e)})`).join("\n"))
|
|
383
|
+
.join("\n");
|
|
384
|
+
const capNote = elements.length >= MAX_REMOVED_UI_ELEMENTS
|
|
385
|
+
? `\n(The list is capped at ${MAX_REMOVED_UI_ELEMENTS} elements; the analyze log records how many were dropped.)`
|
|
386
|
+
: "";
|
|
387
|
+
return `**Removed UI elements — grounding for the removal test.** This diff deletes the elements below (identified by a \`data-*\` attribute) from pages that still exist after the change. The budget rule already requires a test asserting a removed element is absent; this list tells you WHICH elements and on WHICH page, which no blueprint can (the elements are gone). Renames, moved/extracted elements, elements in deleted files, files with no production importer, and values an existing test already references were filtered out server-side; the page is attributed through the import graph (the file, or one hop via the files that import it), not observed.
|
|
388
|
+
|
|
389
|
+
**Removed elements by page:**
|
|
390
|
+
${pageLines}${capNote}
|
|
391
|
+
|
|
392
|
+
**How to record (Task 2), per page — not per element:** if the plan already has a UI candidate on that page, record the guard inside that trace; otherwise register ONE UI candidate \`verify-removed-ui-<page-slug>\` (\`testType: "ui"\`, category \`breaking_change\`; \`skyramp_register_test_plan\` requires at least one step, so use a placeholder \`GET <page path>\` → 200). \`browser_navigate\` to the page and let the app render, then one \`browser_assert_hidden\` call per element using the target shown above — \`testId\` when the attribute is \`data-testid\`, otherwise \`attribute\` + \`value\` (the generated test then uses \`getByTestId(...)\` or \`page.locator('[data-cy="..."]')\` respectively; no \`ref\` — a removed element has no snapshot ref; the tool checks the live page and refuses to record on a blank or un-rendered page), then at least one \`browser_assert\` on a RETAINED element of the same page verifying its text, value or state — the generated \`toBeHidden()\` assertions must never stand alone. Then \`skyramp_export_zip\` and \`skyramp_ui_test_generation\` as usual.
|
|
393
|
+
|
|
394
|
+
**Blueprint Citation for these recommendations:** the removed elements will not appear in any blueprint you capture — that is expected. For a \`data-testid\` element set \`targetElements[].testId\` to the value; for any other attribute leave \`testId\` null and name the \`attribute="value"\` pair in \`reasoning\`. Take \`role\`/\`accessibleName\` from the deleted JSX in the diff, set \`pageContext.url\` to the page above, and prefix the recommendation's \`reasoning\` with \`[removed-element]\`.`;
|
|
395
|
+
}
|
|
362
396
|
/**
|
|
363
397
|
* Instruction block for changed frontend files the server determined have no
|
|
364
398
|
* production importer (SKYR-3855) — dead/orphaned components. Used both to
|
|
@@ -775,6 +809,7 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
775
809
|
}
|
|
776
810
|
let existingTests = [];
|
|
777
811
|
let discoveredRelevantExternalPaths = [];
|
|
812
|
+
let testDiscoveryFailed = false;
|
|
778
813
|
try {
|
|
779
814
|
const testDiscoveryService = new TestDiscoveryService();
|
|
780
815
|
// Run-scoped: only the primary call carries testsRepoDir, so never
|
|
@@ -799,6 +834,7 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
799
834
|
});
|
|
800
835
|
}
|
|
801
836
|
catch (err) {
|
|
837
|
+
testDiscoveryFailed = true;
|
|
802
838
|
logger.warning("Test discovery failed, continuing with empty list", {
|
|
803
839
|
error: err instanceof Error ? err.message : String(err),
|
|
804
840
|
});
|
|
@@ -1379,10 +1415,44 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1379
1415
|
// the pages that import them.
|
|
1380
1416
|
const frontendFileIntegration = checkFrontendFileIntegration(params.repositoryPath, changedFrontendFiles);
|
|
1381
1417
|
const candidateUiPages = await enumerateCandidateUiPages(params.repositoryPath, changedFrontendFiles, frontendFileIntegration);
|
|
1418
|
+
// SKYR-4305: data-testids the diff removed from a page that survives —
|
|
1419
|
+
// grounding for the removal test the budget rule requires (a removed
|
|
1420
|
+
// element appears in no blueprint). Purely additive: a failure here must
|
|
1421
|
+
// never cost the analyze call.
|
|
1422
|
+
let removedElements = [];
|
|
1423
|
+
if (diffData?.diffContent) {
|
|
1424
|
+
try {
|
|
1425
|
+
removedElements = collectRemovedUiElements({
|
|
1426
|
+
changedFrontendFiles,
|
|
1427
|
+
deletedFiles: diffData.deletedFiles ?? [],
|
|
1428
|
+
diffByFile: getDiffByFile(),
|
|
1429
|
+
frontendFileIntegration,
|
|
1430
|
+
candidateUiPages,
|
|
1431
|
+
existingTestFiles: [
|
|
1432
|
+
...existingTests.map((t) => t.testFile),
|
|
1433
|
+
...discoveredRelevantExternalPaths,
|
|
1434
|
+
],
|
|
1435
|
+
discoveryFailed: testDiscoveryFailed,
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
catch (err) {
|
|
1439
|
+
logger.warning("removedUiElements: collection failed, continuing without removal-guard grounding", {
|
|
1440
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1441
|
+
});
|
|
1442
|
+
removedElements = [];
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (removedElements.length > 0) {
|
|
1446
|
+
logger.info("Removed UI elements detected (removal-guard grounding)", {
|
|
1447
|
+
count: removedElements.length,
|
|
1448
|
+
removed: removedElements.map((e) => `${e.attribute}="${e.value}"`),
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1382
1451
|
return {
|
|
1383
1452
|
changedFrontendFiles,
|
|
1384
1453
|
candidateUiPages,
|
|
1385
1454
|
frontendFileIntegration,
|
|
1455
|
+
...(removedElements.length > 0 ? { removedElements } : {}),
|
|
1386
1456
|
};
|
|
1387
1457
|
})();
|
|
1388
1458
|
// Derive the UI blueprint-capture instructions the agent acts on after
|
|
@@ -1407,6 +1477,9 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1407
1477
|
: buildCaptureInstructions(uiContext.candidateUiPages)) +
|
|
1408
1478
|
(unintegratedFrontendFiles.length > 0
|
|
1409
1479
|
? `\n\n${buildUnintegratedInstructions(unintegratedFrontendFiles)}`
|
|
1480
|
+
: "") +
|
|
1481
|
+
(uiContext.removedElements?.length
|
|
1482
|
+
? `\n\n${buildRemovedElementsInstructions(uiContext.removedElements)}`
|
|
1410
1483
|
: "");
|
|
1411
1484
|
// ── SKYR-3879 Path B: budget context + pre-seeded approved plan ──
|
|
1412
1485
|
// Computed BEFORE the state write (not after, alongside the
|
|
@@ -6,7 +6,7 @@ import { ChangedRoute } from "../../utils/changedRoutes.js";
|
|
|
6
6
|
declare const registerCandidateSchema: z.ZodObject<{
|
|
7
7
|
scenarioName: z.ZodString;
|
|
8
8
|
description: z.ZodString;
|
|
9
|
-
category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
|
|
9
|
+
category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
|
|
10
10
|
priority: z.ZodEnum<["high", "medium", "low"]>;
|
|
11
11
|
testType: z.ZodEffects<z.ZodNativeEnum<typeof TestType>, TestType, TestType>;
|
|
12
12
|
steps: z.ZodArray<z.ZodObject<{
|
|
@@ -124,7 +124,7 @@ declare const registerCandidateSchema: z.ZodObject<{
|
|
|
124
124
|
priority: "high" | "medium" | "low";
|
|
125
125
|
testType: TestType;
|
|
126
126
|
scenarioName: string;
|
|
127
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
127
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
128
128
|
steps: {
|
|
129
129
|
path: string;
|
|
130
130
|
method: HttpMethod;
|
|
@@ -160,7 +160,7 @@ declare const registerCandidateSchema: z.ZodObject<{
|
|
|
160
160
|
priority: "high" | "medium" | "low";
|
|
161
161
|
testType: TestType;
|
|
162
162
|
scenarioName: string;
|
|
163
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
163
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
164
164
|
steps: {
|
|
165
165
|
path: string;
|
|
166
166
|
method: HttpMethod;
|
|
@@ -12,7 +12,7 @@ import { CandidateSource, computeCandidateId, scenarioMergeKey, DiscriminatorKin
|
|
|
12
12
|
import { selectPlan } from "../../recommendation/planRanker.js";
|
|
13
13
|
import { resolveSubjectEndpoints } from "../../utils/subjectEndpoints.js";
|
|
14
14
|
import { collectChangedRouteLines } from "../../utils/changedRoutes.js";
|
|
15
|
-
import {
|
|
15
|
+
import { generateFloors } from "../../recommendation/budgeters/shared.js";
|
|
16
16
|
import { inferScenarioType } from "../../recommendation/diversity.js";
|
|
17
17
|
import { validateDiscriminator } from "../../recommendation/discriminators.js";
|
|
18
18
|
import { isAttackSurfaceSecurityBoundary } from "../../prompts/test-recommendation/recommendationShared.js";
|
|
@@ -76,7 +76,7 @@ const discriminatorClaimSchema = z.object({
|
|
|
76
76
|
const registerCandidateSchema = z.object({
|
|
77
77
|
scenarioName: z.string().min(1).describe("Name for this test/scenario. Reused as the match key against the generation tool's scenarioName — keep it identical when you later call the generation tool."),
|
|
78
78
|
description: z.string().min(1),
|
|
79
|
-
category: z.enum(SCENARIO_CATEGORIES).describe("Test category — drives ranking (never your own priority tag)."),
|
|
79
|
+
category: z.enum(SCENARIO_CATEGORIES).describe("Test category — drives ranking (never your own priority tag). `bug_caught` targets a `<bug_found>` flaw from code review. `requirement_conflict` asserts a requirement the PR title/description (or a requirements file it references) states and the implemented behavior contradicts — it fails on the current code by design. Both rank in the top tier but are carved out independently, so a requirement conflict never loses its slot to a code-review bug."),
|
|
80
80
|
priority: z.enum(["high", "medium", "low"]).describe("Your own assessment — recorded, but ranking is derived from category, not this field."),
|
|
81
81
|
testType: z.nativeEnum(TestType).refine((t) => PLAN_TEST_TYPES.has(t), {
|
|
82
82
|
message: `testType must be one of ${[...PLAN_TEST_TYPES].join(", ")}`,
|
|
@@ -340,7 +340,26 @@ function renderGenerationDirective(plan) {
|
|
|
340
340
|
(nonUICount > 0 ? ` Generate the ${tests(nonUICount)} of other types as well.` : ""),
|
|
341
341
|
];
|
|
342
342
|
}
|
|
343
|
-
|
|
343
|
+
/** SKYR-4292: the server cannot count the diff's changed behaviors, so an
|
|
344
|
+
* under-budget plan is flagged (not rejected) with what a missing candidate looks
|
|
345
|
+
* like. Wipro PR #24 registered 3 candidates against a 20-slot UI-only budget and
|
|
346
|
+
* the tool answered "AUTHORITATIVE and FINAL" with no pushback. */
|
|
347
|
+
function renderBudgetCheck(plan, maxTotal) {
|
|
348
|
+
const filled = plan.generate.length + plan.additional.length;
|
|
349
|
+
if (filled >= maxTotal)
|
|
350
|
+
return [];
|
|
351
|
+
return [
|
|
352
|
+
"",
|
|
353
|
+
`### Budget check: ${filled} of ${maxTotal} slots filled`,
|
|
354
|
+
"Every observable behavior this diff changes — each option value added, removed, renamed or reordered; each label, " +
|
|
355
|
+
"element, route, state, validation rule, computed value or style value (spacing, size, color, radius, font weight, " +
|
|
356
|
+
"class name) that renders differently; each removed element, asserted absent — needs a candidate above, however " +
|
|
357
|
+
"small the change. A change is covered only when an existing test asserts the changed value, not when a test merely " +
|
|
358
|
+
"reaches the component. If any changed behavior has no candidate, call skyramp_register_test_plan again with your " +
|
|
359
|
+
"full updated list. If the diff truly changes fewer behaviors than the budget, this plan stands as-is.",
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
function renderPlanText(plan, dropped, maxTotal) {
|
|
344
363
|
const lines = [];
|
|
345
364
|
lines.push(`## Approved Test Plan (${plan.planId})`);
|
|
346
365
|
lines.push("");
|
|
@@ -402,6 +421,7 @@ function renderPlanText(plan, dropped) {
|
|
|
402
421
|
}
|
|
403
422
|
lines.push("");
|
|
404
423
|
lines.push(...renderGenerationDirective(plan));
|
|
424
|
+
lines.push(...renderBudgetCheck(plan, maxTotal));
|
|
405
425
|
return lines.join("\n");
|
|
406
426
|
}
|
|
407
427
|
// ── Tool registration ───────────────────────────────────────────────────────
|
|
@@ -544,10 +564,11 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
544
564
|
// changes would silently plan zero UI tests (runs 32419025266,
|
|
545
565
|
// 32431042133, 32431106864) — reject so the agent self-corrects in one
|
|
546
566
|
// step, exactly like the empty-candidates rejection above.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
errorResult = toolError(
|
|
550
|
-
"
|
|
567
|
+
const uiFloor = generateFloors(budgetContext).ui;
|
|
568
|
+
if (uiFloor > 0 && !allCandidates.some((c) => inferScenarioType(c.scenario) === TestType.UI)) {
|
|
569
|
+
errorResult = toolError(`This run has frontend changes, so the plan MUST include UI tests — at least ${uiFloor} of the ` +
|
|
570
|
+
"GENERATE slots are held for them, and more are open to UI candidates on rank. No submitted or " +
|
|
571
|
+
"recovered candidate has testType 'ui'. Resubmit your full candidate list including UI candidates " +
|
|
551
572
|
"for the changed frontend (see uiContext.changedFrontendFiles).");
|
|
552
573
|
return errorResult;
|
|
553
574
|
}
|
|
@@ -576,7 +597,7 @@ export function registerRegisterTestPlanTool(server) {
|
|
|
576
597
|
return errorResult;
|
|
577
598
|
}
|
|
578
599
|
return {
|
|
579
|
-
content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped) }],
|
|
600
|
+
content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped, budgetContext.maxTotal) }],
|
|
580
601
|
};
|
|
581
602
|
}
|
|
582
603
|
catch (error) {
|
|
@@ -1119,7 +1119,7 @@ export declare const scenarioStepSchema: z.ZodObject<{
|
|
|
1119
1119
|
export declare const draftedScenarioSchema: z.ZodObject<{
|
|
1120
1120
|
scenarioName: z.ZodString;
|
|
1121
1121
|
description: z.ZodString;
|
|
1122
|
-
category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
|
|
1122
|
+
category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
|
|
1123
1123
|
priority: z.ZodEnum<["high", "medium", "low"]>;
|
|
1124
1124
|
steps: z.ZodArray<z.ZodObject<{
|
|
1125
1125
|
order: z.ZodNumber;
|
|
@@ -1231,7 +1231,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
|
|
|
1231
1231
|
description: string;
|
|
1232
1232
|
priority: "high" | "medium" | "low";
|
|
1233
1233
|
scenarioName: string;
|
|
1234
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1234
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1235
1235
|
steps: {
|
|
1236
1236
|
path: string;
|
|
1237
1237
|
method: string;
|
|
@@ -1268,7 +1268,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
|
|
|
1268
1268
|
description: string;
|
|
1269
1269
|
priority: "high" | "medium" | "low";
|
|
1270
1270
|
scenarioName: string;
|
|
1271
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1271
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1272
1272
|
steps: {
|
|
1273
1273
|
path: string;
|
|
1274
1274
|
method: string;
|
|
@@ -1607,7 +1607,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
1607
1607
|
draftedScenarios: z.ZodArray<z.ZodObject<{
|
|
1608
1608
|
scenarioName: z.ZodString;
|
|
1609
1609
|
description: z.ZodString;
|
|
1610
|
-
category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
|
|
1610
|
+
category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
|
|
1611
1611
|
priority: z.ZodEnum<["high", "medium", "low"]>;
|
|
1612
1612
|
steps: z.ZodArray<z.ZodObject<{
|
|
1613
1613
|
order: z.ZodNumber;
|
|
@@ -1719,7 +1719,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
1719
1719
|
description: string;
|
|
1720
1720
|
priority: "high" | "medium" | "low";
|
|
1721
1721
|
scenarioName: string;
|
|
1722
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1722
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1723
1723
|
steps: {
|
|
1724
1724
|
path: string;
|
|
1725
1725
|
method: string;
|
|
@@ -1756,7 +1756,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
1756
1756
|
description: string;
|
|
1757
1757
|
priority: "high" | "medium" | "low";
|
|
1758
1758
|
scenarioName: string;
|
|
1759
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1759
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1760
1760
|
steps: {
|
|
1761
1761
|
path: string;
|
|
1762
1762
|
method: string;
|
|
@@ -1799,7 +1799,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
1799
1799
|
description: string;
|
|
1800
1800
|
priority: "high" | "medium" | "low";
|
|
1801
1801
|
scenarioName: string;
|
|
1802
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1802
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1803
1803
|
steps: {
|
|
1804
1804
|
path: string;
|
|
1805
1805
|
method: string;
|
|
@@ -1842,7 +1842,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
1842
1842
|
description: string;
|
|
1843
1843
|
priority: "high" | "medium" | "low";
|
|
1844
1844
|
scenarioName: string;
|
|
1845
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1845
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
1846
1846
|
steps: {
|
|
1847
1847
|
path: string;
|
|
1848
1848
|
method: string;
|
|
@@ -2743,7 +2743,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
2743
2743
|
description: string;
|
|
2744
2744
|
priority: "high" | "medium" | "low";
|
|
2745
2745
|
scenarioName: string;
|
|
2746
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
2746
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
2747
2747
|
steps: {
|
|
2748
2748
|
path: string;
|
|
2749
2749
|
method: string;
|
|
@@ -2956,7 +2956,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
2956
2956
|
description: string;
|
|
2957
2957
|
priority: "high" | "medium" | "low";
|
|
2958
2958
|
scenarioName: string;
|
|
2959
|
-
category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
2959
|
+
category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
|
|
2960
2960
|
steps: {
|
|
2961
2961
|
path: string;
|
|
2962
2962
|
method: string;
|
|
@@ -41,6 +41,18 @@ export interface MaintenanceActionCore {
|
|
|
41
41
|
* downstream stage that reasons about the edit needs both — notably the report-time
|
|
42
42
|
* working-tree check, which otherwise calls a POM-backed UPDATE unbacked (SKYR-4129). */
|
|
43
43
|
pomFile?: string;
|
|
44
|
+
/** Visual-snapshot baselines (toHaveScreenshot filenames, e.g. "page-001.png") an UPDATE
|
|
45
|
+
* refreshes because the diff changed how the captured page/element looks (SKYR-4298).
|
|
46
|
+
* The edit lands in the PNG under `<spec>-snapshots/`, not in the spec, so the
|
|
47
|
+
* report-time working-tree check must accept that PNG as the UPDATE's backing, and
|
|
48
|
+
* the final `skyramp_execute_test` must receive the list as `rebaselineSnapshots`. */
|
|
49
|
+
rebaselineSnapshots?: string[];
|
|
50
|
+
/** True when the UPDATE carries rebaselineSnapshots and no updateInstructions: the
|
|
51
|
+
* refresh is its whole maintenance, so no edit to the spec/POM is expected. When false
|
|
52
|
+
* (or absent) with baselines listed, the report-time check requires BOTH the edit and
|
|
53
|
+
* the rewritten PNG — a listed baseline must never exempt the selector edit the same
|
|
54
|
+
* verdict claimed (SKYR-3883 stays in force). */
|
|
55
|
+
rebaselineOnly?: boolean;
|
|
44
56
|
}
|
|
45
57
|
/** Normalized internal recommendation built from LLM-supplied args.recommendations. */
|
|
46
58
|
export interface DriftRecommendation extends MaintenanceActionCore {
|
|
@@ -60,6 +60,10 @@ export interface TestExecutionOptions {
|
|
|
60
60
|
playwrightSaveStoragePath?: string;
|
|
61
61
|
dockerNetwork?: string;
|
|
62
62
|
useHostNetwork?: boolean;
|
|
63
|
+
/** Visual-snapshot baselines (toHaveScreenshot filenames, e.g. "page-001.png") this run
|
|
64
|
+
* replaces instead of comparing against — forwarded to SmartPlaywright as
|
|
65
|
+
* SKYRAMP_UPDATE_SNAPSHOTS (SKYR-4298). Only for an intended UI change the diff explains. */
|
|
66
|
+
rebaselineSnapshots?: string[];
|
|
63
67
|
}
|
|
64
68
|
/**
|
|
65
69
|
* Progress callback for reporting execution status
|
|
@@ -12,7 +12,7 @@ export declare enum Novelty {
|
|
|
12
12
|
EXISTING = "existing"
|
|
13
13
|
}
|
|
14
14
|
/** All categories including internal ones. */
|
|
15
|
-
export declare const SCENARIO_CATEGORIES: readonly ["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
|
|
15
|
+
export declare const SCENARIO_CATEGORIES: readonly ["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
|
|
16
16
|
export type ScenarioCategory = typeof SCENARIO_CATEGORIES[number];
|
|
17
17
|
/** Categories valid for tool submissions (excludes internal-only categories). */
|
|
18
18
|
export declare const TEST_CATEGORIES: readonly ["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
|
|
@@ -21,3 +21,13 @@ export type TestCategory = typeof TEST_CATEGORIES[number];
|
|
|
21
21
|
export declare const CATEGORY_PRIORITY: Record<ScenarioCategory, PriorityTier>;
|
|
22
22
|
/** Map internal-only categories to their external equivalent for tool submission. */
|
|
23
23
|
export declare function externalCategory(cat: ScenarioCategory): TestCategory;
|
|
24
|
+
/**
|
|
25
|
+
* Categories whose scenarios target a specific identified defect — a code flaw
|
|
26
|
+
* (`bug_caught`) or a stated requirement the code contradicts
|
|
27
|
+
* (`requirement_conflict`). They survive external-test dedup: an existing test on
|
|
28
|
+
* the same endpoint exercises the surface, not the flaw, so removing them would
|
|
29
|
+
* drop the only test that fails on the defect.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FLAW_TARGETING_CATEGORIES: readonly ["bug_caught", "requirement_conflict"];
|
|
32
|
+
/** Whether `category` targets a specific identified defect (see {@link FLAW_TARGETING_CATEGORIES}). */
|
|
33
|
+
export declare function isFlawTargetingCategory(category: ScenarioCategory | undefined): boolean;
|
|
@@ -17,6 +17,14 @@ export var Novelty;
|
|
|
17
17
|
const INTERNAL_CATEGORIES = [
|
|
18
18
|
"new_endpoint", // MEDIUM - diff-direct scenario; where a test came from, not a guarantee of a slot
|
|
19
19
|
"bug_caught", // CRITICAL - tests targeting a specific <bug_found> flaw identified during enrichment
|
|
20
|
+
// CRITICAL - tests asserting a requirement the PR title/description (or a
|
|
21
|
+
// requirements file it references) states, which the implemented behavior
|
|
22
|
+
// contradicts. Separate from bug_caught deliberately (SKYR-4291): labelled
|
|
23
|
+
// bug_caught, a requirement-vs-code mismatch competed with the code-review
|
|
24
|
+
// flaws for the same promotion and lost it on severity. Its own category means
|
|
25
|
+
// its own carve-out here, first place in the promotion order SKYR-4275's bound
|
|
26
|
+
// hands out, and its own coverage gate.
|
|
27
|
+
"requirement_conflict",
|
|
20
28
|
];
|
|
21
29
|
/** External categories valid for tool submissions, ordered by priority. */
|
|
22
30
|
const CATEGORIES = [
|
|
@@ -59,6 +67,12 @@ export const CATEGORY_PRIORITY = {
|
|
|
59
67
|
// fill GENERATE.
|
|
60
68
|
new_endpoint: PriorityTier.MEDIUM,
|
|
61
69
|
bug_caught: PriorityTier.CRITICAL, // tests targeting a <bug_found> flaw — always in GENERATE
|
|
70
|
+
// A stated requirement the implementation contradicts is the point of the PR,
|
|
71
|
+
// so it sits in the same top tier as bug_caught. Being its own category, it is
|
|
72
|
+
// carved out separately in planRanker and takes the FIRST slot of the promotion
|
|
73
|
+
// bound SKYR-4275 sets — the code-review flaws promote into what is left, so a
|
|
74
|
+
// requirement conflict is never the finding that loses on severity.
|
|
75
|
+
requirement_conflict: PriorityTier.CRITICAL,
|
|
62
76
|
business_rule: PriorityTier.HIGH, // formula/business-logic bugs are high priority
|
|
63
77
|
security_boundary: PriorityTier.HIGH,
|
|
64
78
|
data_integrity: PriorityTier.HIGH,
|
|
@@ -75,5 +89,25 @@ export function externalCategory(cat) {
|
|
|
75
89
|
return "crud";
|
|
76
90
|
if (cat === "bug_caught")
|
|
77
91
|
return "business_rule";
|
|
92
|
+
// The stated requirement IS the business rule the test asserts — same landing
|
|
93
|
+
// spot as bug_caught, so a requirement-conflict test reads as a rule check in
|
|
94
|
+
// the customer-facing report rather than an unrecognised label.
|
|
95
|
+
if (cat === "requirement_conflict")
|
|
96
|
+
return "business_rule";
|
|
78
97
|
return cat;
|
|
79
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Categories whose scenarios target a specific identified defect — a code flaw
|
|
101
|
+
* (`bug_caught`) or a stated requirement the code contradicts
|
|
102
|
+
* (`requirement_conflict`). They survive external-test dedup: an existing test on
|
|
103
|
+
* the same endpoint exercises the surface, not the flaw, so removing them would
|
|
104
|
+
* drop the only test that fails on the defect.
|
|
105
|
+
*/
|
|
106
|
+
export const FLAW_TARGETING_CATEGORIES = [
|
|
107
|
+
"bug_caught",
|
|
108
|
+
"requirement_conflict",
|
|
109
|
+
];
|
|
110
|
+
/** Whether `category` targets a specific identified defect (see {@link FLAW_TARGETING_CATEGORIES}). */
|
|
111
|
+
export function isFlawTargetingCategory(category) {
|
|
112
|
+
return category !== undefined && FLAW_TARGETING_CATEGORIES.includes(category);
|
|
113
|
+
}
|
|
@@ -5,6 +5,7 @@ import { RepositoryAnalysis, AnalysisScope } from "../types/RepositoryAnalysis.j
|
|
|
5
5
|
import { PRTestContext } from "./pr-comment-parser.js";
|
|
6
6
|
import { ParsedDiffEndpoint } from "./routeParsers.js";
|
|
7
7
|
import type { CandidateUiPage } from "./uiPageEnumerator.js";
|
|
8
|
+
import type { RemovedUiElement } from "./removedUiElements.js";
|
|
8
9
|
import type { FrontendFileIntegration } from "../types/FrontendIntegration.js";
|
|
9
10
|
import type { ApprovedPlanItem } from "../types/Recommendation.js";
|
|
10
11
|
import type { ExternalTestRunRecord } from "../types/ExternalTestExecution.js";
|
|
@@ -135,6 +136,12 @@ export interface UiAnalysisContext {
|
|
|
135
136
|
* the agent running its own grep mid-task. Same order as changedFrontendFiles.
|
|
136
137
|
*/
|
|
137
138
|
frontendFileIntegration?: FrontendFileIntegration[];
|
|
139
|
+
/**
|
|
140
|
+
* data-testids the diff removed from files whose page survives (SKYR-4305).
|
|
141
|
+
* Present only when non-empty. The testbot records a removal guard
|
|
142
|
+
* (`toBeHidden()` on the id + a positive sibling assertion) for each entry.
|
|
143
|
+
*/
|
|
144
|
+
removedElements?: RemovedUiElement[];
|
|
138
145
|
}
|
|
139
146
|
/**
|
|
140
147
|
* SKYR-3879 Path B — the server-side selection checkpoint's persisted result.
|