@skyramp/mcp 0.3.8 → 0.4.0-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/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- 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 +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- 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 +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* consumers — planGuard (generation-tool hard gate) and submitReportTool
|
|
6
|
-
* (report cross-check) — only ever do a Set/array membership check, never
|
|
7
|
-
* re-derive normalization logic against a live candidate.
|
|
2
|
+
* Shared match-key computation for the generation-tool plan gate. The generation
|
|
3
|
+
* tools are called with a scenario name and an endpoint, never a `plannedTestId`,
|
|
4
|
+
* so this joins a call back to a plan planned test; the report compares ids.
|
|
8
5
|
*
|
|
9
6
|
* Two kinds of key:
|
|
10
7
|
* - `name:<slug>` — normalized scenarioName, for multi-step
|
|
11
|
-
*
|
|
8
|
+
* planned tests (batch-scenario / integration) where the agent's own name is
|
|
12
9
|
* the natural join key.
|
|
13
10
|
* - `ep:<testType>:<METHOD>:<path>` — one per step, for single-endpoint
|
|
14
11
|
* matching (contract tests, or a fallback when names drift between
|
|
15
12
|
* register and generate).
|
|
16
13
|
*/
|
|
17
|
-
import { isParamSegment, isOpaqueIdSegment } from "./routeParsers.js";
|
|
18
14
|
/** Lowercase, collapse non-alphanumeric runs to '-', trim leading/trailing '-'. */
|
|
19
15
|
export function slugifyName(name) {
|
|
20
16
|
return (name ?? "")
|
|
@@ -23,25 +19,45 @@ export function slugifyName(name) {
|
|
|
23
19
|
.replace(/^-+|-+$/g, "");
|
|
24
20
|
}
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
22
|
+
* One path segment that names a parameter rather than a resource, in any of the
|
|
23
|
+
* route syntaxes a repository writes: OpenAPI "{id}", Express/NestJS ":id", and Next.js
|
|
24
|
+
* "[id]" / "[...id]" / "[[...id]]" (the double-bracket form needs its own
|
|
25
|
+
* alternative — it contains an inner "]", which the single-bracket form's
|
|
26
|
+
* [^\]]+ can never span).
|
|
27
|
+
*/
|
|
28
|
+
// The colon form may carry an Express/Fastify constraint or modifier —
|
|
29
|
+
// `:id(\d+)`, `:id?`, `:id*`, `:id+`. Those are still parameters, and a
|
|
30
|
+
// resource name is never one, so the predicate has to accept them, or a
|
|
31
|
+
// constrained param stops collapsing and its route stops matching.
|
|
32
|
+
const PARAM_SEGMENT_RE = /^\{[^}]+\}$|^:[A-Za-z_]\w*(?:\([^)]*\))?[?*+]?$|^\[\[[^\]]+\]\]$|^\[[^\]]+\]$/;
|
|
33
|
+
export function isParamSegment(segment) {
|
|
34
|
+
return PARAM_SEGMENT_RE.test(segment);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One path segment that is an id VALUE rather than a name: a UUID, digits only,
|
|
38
|
+
* or a long hex run (a Mongo ObjectId is 24). A caller writes a literal id in a
|
|
39
|
+
* path where the route declares a parameter, and an id can never be a resource
|
|
40
|
+
* name, so resource derivation must skip it exactly as it skips a parameter.
|
|
41
|
+
* SKYR-4214: an accepted UUID produced the key
|
|
42
|
+
* "DELETE::a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11::integration", which matches
|
|
43
|
+
* nothing, so the planned test escaped dedup entirely.
|
|
44
|
+
*
|
|
45
|
+
* The 16-character floor on the hex rule keeps real resource names safe: a name
|
|
46
|
+
* built only from the letters a-f and digits, 16 characters or longer, is not a
|
|
47
|
+
* word.
|
|
48
|
+
*/
|
|
49
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
50
|
+
export function isOpaqueIdSegment(segment) {
|
|
51
|
+
// The sign belongs to the value: an edge-case step writes `/orders/-1` to
|
|
52
|
+
// probe a negative id, and without the sign that segment became the resource
|
|
53
|
+
// name, so the key could never match real order coverage.
|
|
54
|
+
return UUID_RE.test(segment) || /^-?\d+$/.test(segment) || /^[0-9a-f]{16,}$/i.test(segment);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Normalize a path so param and id segments collapse to `:p`.
|
|
58
|
+
*
|
|
59
|
+
* The two predicates above are the only rule for it — an earlier inline copy had
|
|
60
|
+
* no bracket rule, so a Next.js `[id]` stopped matching its `{id}` spelling.
|
|
45
61
|
*/
|
|
46
62
|
export function normalizeMatchPath(rawPath) {
|
|
47
63
|
const pathPart = (rawPath ?? "").split("?")[0];
|
|
@@ -50,58 +66,43 @@ export function normalizeMatchPath(rawPath) {
|
|
|
50
66
|
.map((seg) => {
|
|
51
67
|
if (seg === "")
|
|
52
68
|
return seg;
|
|
53
|
-
// `isParamSegment` covers
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// or a registered `/users/:1abc` stops endpoint-matching `/users/42`.
|
|
69
|
+
// `isParamSegment` covers only well-formed colon params. The broader
|
|
70
|
+
// `/^:/` test in front of it catches the ones that regex rejects, such as
|
|
71
|
+
// `:1abc` or `:id-x`: a malformed param must still normalize to `:p`, or
|
|
72
|
+
// a registered `/users/:1abc` stops endpoint-matching `/users/42`.
|
|
58
73
|
if (/^:/.test(seg) || isParamSegment(seg) || isOpaqueIdSegment(seg))
|
|
59
74
|
return ":p";
|
|
60
75
|
return seg.toLowerCase();
|
|
61
76
|
})
|
|
62
77
|
.join("/");
|
|
63
78
|
}
|
|
79
|
+
/** The scenario is read back from the stored plan, so `steps` is an array only on
|
|
80
|
+
* the validated path. `?? []` keeps a malformed plan from costing the call. */
|
|
64
81
|
function inferTestType(scenario) {
|
|
65
|
-
|
|
82
|
+
const steps = Array.isArray(scenario?.steps) ? scenario.steps : [];
|
|
83
|
+
return (scenario?.testType ?? (steps.length === 1 ? "contract" : "integration")).toLowerCase();
|
|
66
84
|
}
|
|
67
|
-
/** Name key (if a name exists) plus one endpoint key per step.
|
|
85
|
+
/** Name key (if a name exists) plus one endpoint key per step. BOTH kinds carry
|
|
86
|
+
* the planned test's test type, or a contract call matches an integration-only
|
|
87
|
+
* planned test sharing its name. A slug holds no `:`. */
|
|
68
88
|
export function computeMatchKeys(scenario) {
|
|
69
89
|
const keys = [];
|
|
70
|
-
const slug = slugifyName(scenario.scenarioName);
|
|
71
|
-
if (slug)
|
|
72
|
-
keys.push(`name:${slug}`);
|
|
73
90
|
const testType = inferTestType(scenario);
|
|
74
|
-
|
|
91
|
+
const slug = slugifyName(scenario?.scenarioName);
|
|
92
|
+
if (slug)
|
|
93
|
+
keys.push(`name:${testType}:${slug}`);
|
|
94
|
+
for (const step of Array.isArray(scenario?.steps) ? scenario.steps : []) {
|
|
75
95
|
if (!step?.method || !step?.path)
|
|
76
96
|
continue;
|
|
77
97
|
keys.push(`ep:${testType}:${step.method.toUpperCase()}:${normalizeMatchPath(step.path)}`);
|
|
78
98
|
}
|
|
79
99
|
return [...new Set(keys)];
|
|
80
100
|
}
|
|
81
|
-
/** Build the persisted plan item from a ranked Candidate. */
|
|
82
|
-
export function buildApprovedPlanItem(candidate) {
|
|
83
|
-
return {
|
|
84
|
-
candidateId: candidate.candidateId,
|
|
85
|
-
scenarioName: candidate.scenario.scenarioName,
|
|
86
|
-
testType: inferTestType(candidate.scenario),
|
|
87
|
-
category: candidate.scenario.category,
|
|
88
|
-
source: candidate.source,
|
|
89
|
-
...(candidate.verifiedDiscriminator ? { verifiedDiscriminator: candidate.verifiedDiscriminator } : {}),
|
|
90
|
-
...(candidate.scenario.subjectEndpoints !== undefined
|
|
91
|
-
? { subjectEndpoints: candidate.scenario.subjectEndpoints }
|
|
92
|
-
: {}),
|
|
93
|
-
matchKeys: computeMatchKeys(candidate.scenario),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
101
|
/**
|
|
97
|
-
* Segment-aligned comparison of two normalized paths
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* (`/orders/{id}`) while the generation tool's query comes from the live
|
|
102
|
-
* endpointURL (`/api/v1/orders/{id}`). The shorter path matches when its
|
|
103
|
-
* segments are a suffix of the longer path's segments — never mid-segment,
|
|
104
|
-
* and never on an all-parameter path (`/:p` would match everything).
|
|
102
|
+
* Segment-aligned comparison of two normalized paths, tolerating a MOUNT prefix
|
|
103
|
+
* on either side. The shorter must be the whole tail of the longer, never
|
|
104
|
+
* mid-segment and never all-parameter, and the extra leading segments must hold
|
|
105
|
+
* no parameter — which separates `/api/v1` + `/orders` from `/tenants/{id}` + it.
|
|
105
106
|
*/
|
|
106
107
|
function normalizedPathsMatch(a, b) {
|
|
107
108
|
if (a === b)
|
|
@@ -114,21 +115,55 @@ function normalizedPathsMatch(a, b) {
|
|
|
114
115
|
if (shorter.every((seg) => seg === ":p"))
|
|
115
116
|
return false;
|
|
116
117
|
const offset = longer.length - shorter.length;
|
|
117
|
-
|
|
118
|
+
if (!shorter.every((seg, i) => longer[offset + i] === seg))
|
|
119
|
+
return false;
|
|
120
|
+
// The extra leading segments are the planned test mount. A parameter among them
|
|
121
|
+
// means the two paths address different resources.
|
|
122
|
+
return longer.slice(0, offset).every((seg) => seg !== ":p");
|
|
123
|
+
}
|
|
124
|
+
/** Whether `keys` hold this query's name channel — the same test
|
|
125
|
+
* `matchesMatchKeys` applies before it consults the endpoint. */
|
|
126
|
+
function nameAgrees(keys, query) {
|
|
127
|
+
const nameSlug = query.scenarioName ? slugifyName(query.scenarioName) : "";
|
|
128
|
+
if (!nameSlug)
|
|
129
|
+
return false;
|
|
130
|
+
const wanted = query.testType ? `name:${query.testType.toLowerCase()}:${nameSlug}` : undefined;
|
|
131
|
+
return keys.some((key) => wanted !== undefined ? key === wanted : key.startsWith("name:") && key.endsWith(`:${nameSlug}`));
|
|
132
|
+
}
|
|
133
|
+
/** Whether the plan holds this query's endpoint, ignoring the name. Shared by
|
|
134
|
+
* the name channel — which may no longer answer alone on an endpoint query —
|
|
135
|
+
* and by the endpoint channel below. */
|
|
136
|
+
function endpointAgrees(keys, query) {
|
|
137
|
+
if (!query.path)
|
|
138
|
+
return false;
|
|
139
|
+
const normPath = normalizeMatchPath(query.path);
|
|
140
|
+
const methodFilter = query.method ? query.method.toUpperCase() : undefined;
|
|
141
|
+
return keys.some((key) => {
|
|
142
|
+
if (!key.startsWith("ep:"))
|
|
143
|
+
return false;
|
|
144
|
+
const parts = key.split(":");
|
|
145
|
+
if (methodFilter && parts[2] !== methodFilter)
|
|
146
|
+
return false;
|
|
147
|
+
return normalizedPathsMatch(parts.slice(3).join(":"), normPath);
|
|
148
|
+
});
|
|
118
149
|
}
|
|
119
150
|
/**
|
|
120
|
-
* True when `query` matches
|
|
121
|
-
* - by scenarioName slug (`name:<slug>`), or
|
|
151
|
+
* True when `query` matches `keys`:
|
|
152
|
+
* - by testType + scenarioName slug (`name:<testType>:<slug>`), or
|
|
122
153
|
* - by testType + path (`ep:<testType>:*:<path>`), method-exact when
|
|
123
154
|
* `query.method` is given, method-agnostic (any method) otherwise — the
|
|
124
155
|
* contract tool's `method: ""` ("test all methods") case. Path comparison
|
|
125
156
|
* tolerates a base-path prefix on either side (see normalizedPathsMatch).
|
|
157
|
+
* The planned test's test type gates BOTH channels: gating the endpoint channel
|
|
158
|
+
* alone let a contract call pass against an integration-only planned test of the
|
|
159
|
+
* same name. Where the query carries an endpoint the name match must agree on it
|
|
160
|
+
* too, since `slugifyName` folds case and every run of non-alphanumerics.
|
|
126
161
|
*/
|
|
127
|
-
export function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
162
|
+
export function matchesMatchKeys(keys, query) {
|
|
163
|
+
if (nameAgrees(keys, query)) {
|
|
164
|
+
if (!query.path)
|
|
165
|
+
return true;
|
|
166
|
+
if (endpointAgrees(keys, query))
|
|
132
167
|
return true;
|
|
133
168
|
}
|
|
134
169
|
if (query.testType && query.path) {
|
|
@@ -150,22 +185,49 @@ export function matchesApprovedItem(items, query) {
|
|
|
150
185
|
return false;
|
|
151
186
|
return normalizedPathsMatch(keyPath, normPath);
|
|
152
187
|
};
|
|
153
|
-
if (
|
|
188
|
+
if (keys.some(epKeyMatches))
|
|
154
189
|
return true;
|
|
155
190
|
}
|
|
156
191
|
return false;
|
|
157
192
|
}
|
|
158
193
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* for a config-only PR) there is nothing to backfill, so ADDITIONAL must not
|
|
164
|
-
* admit generation. Shared by planGuard and submitReportTool so the gate and
|
|
165
|
-
* the report cross-check can never disagree on admissibility.
|
|
194
|
+
* The planned tests whose scenarioName and testType match but whose planned steps
|
|
195
|
+
* name no endpoint the query addresses — the near miss a boolean cannot report,
|
|
196
|
+
* where the real disagreement is one step's path spelling. Empty when the query
|
|
197
|
+
* names no path.
|
|
166
198
|
*/
|
|
167
|
-
export function
|
|
168
|
-
if (
|
|
169
|
-
return
|
|
170
|
-
return
|
|
199
|
+
export function nameMatchedEndpointMismatches(plannedTests, query) {
|
|
200
|
+
if (!query.path)
|
|
201
|
+
return [];
|
|
202
|
+
return plannedTests.filter((plannedTest) => {
|
|
203
|
+
if (!plannedTest?.scenario)
|
|
204
|
+
return false;
|
|
205
|
+
const keys = computeMatchKeys(plannedTest.scenario);
|
|
206
|
+
return nameAgrees(keys, query) && !endpointAgrees(keys, query);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** The planned tests whose scenarioName matches but whose testType does not — the
|
|
210
|
+
* plan holds this scenario and the call asked a different generator for it, so
|
|
211
|
+
* "not in the plan" is the wrong thing to say. Empty when the query names no
|
|
212
|
+
* type, since `nameAgrees` then matches on the name alone. */
|
|
213
|
+
export function nameMatchedTypeMismatches(plannedTests, query) {
|
|
214
|
+
const nameSlug = query.scenarioName ? slugifyName(query.scenarioName) : "";
|
|
215
|
+
if (!nameSlug || !query.testType)
|
|
216
|
+
return [];
|
|
217
|
+
const calledType = query.testType.toLowerCase();
|
|
218
|
+
return plannedTests.filter((plannedTest) => {
|
|
219
|
+
if (!plannedTest?.scenario)
|
|
220
|
+
return false;
|
|
221
|
+
const keys = computeMatchKeys(plannedTest.scenario);
|
|
222
|
+
// The name channel, minus the type: a key ending in this slug means the plan
|
|
223
|
+
// holds the scenario, and the segment before it is the type it holds it as.
|
|
224
|
+
const named = keys.filter((key) => key.startsWith("name:") && key.endsWith(`:${nameSlug}`));
|
|
225
|
+
return named.length > 0 && !named.some((key) => key === `name:${calledType}:${nameSlug}`);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** True when `query` matches at least one planned test of the registered plan.
|
|
229
|
+
* The keys are computed per call rather than stored with the plan, so there is
|
|
230
|
+
* one description of a planned test and no second shape to drift from it. */
|
|
231
|
+
export function matchesPlanCandidates(plannedTests, query) {
|
|
232
|
+
return plannedTests.some((plannedTest) => !!plannedTest?.scenario && matchesMatchKeys(computeMatchKeys(plannedTest.scenario), query));
|
|
171
233
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Visual-snapshot baseline names as a Skyramp test passes them to
|
|
4
|
+
* `toHaveScreenshot()` — e.g. `page-001.png` — and as `skyramp_actions` and
|
|
5
|
+
* `skyramp_execute_test` accept them in `rebaselineSnapshots` (SKYR-4298).
|
|
6
|
+
*
|
|
7
|
+
* Bare filename only: no path separators, and no commas or whitespace, because the
|
|
8
|
+
* list is joined with commas into `SKYRAMP_UPDATE_SNAPSHOTS` and split again in the
|
|
9
|
+
* executor, where `a,b.png` would read as two baselines. One definition shared by
|
|
10
|
+
* both tools so they can never drift apart.
|
|
11
|
+
*/
|
|
12
|
+
export declare const REBASELINE_SNAPSHOT_NAME_RE: RegExp;
|
|
13
|
+
export declare const rebaselineSnapshotNameSchema: z.ZodString;
|
|
14
|
+
/** `page-001.png` → `page-001`. */
|
|
15
|
+
export declare function baselineStem(name: string): string;
|
|
16
|
+
export declare function baselineFileMatchesStem(fileBasename: string, stem: string): boolean;
|
|
17
|
+
/** The directory Playwright keeps a spec's baselines in under the default layout. */
|
|
18
|
+
export declare function snapshotDirFor(specFile: string): string;
|
|
19
|
+
/** Looks like an on-disk baseline filename (`page-001-chromium-linux.png`) rather than the
|
|
20
|
+
* name the test passes (`page-001.png`) — the likeliest wrong input, and one the name
|
|
21
|
+
* schema accepts. Advisory only: `hero-linux.png` is also a perfectly valid name a test
|
|
22
|
+
* can pass, so callers warn and keep the entry; execution authorizes it against the
|
|
23
|
+
* verdict and reports whether anything was actually rewritten. */
|
|
24
|
+
export declare function looksLikeOnDiskBaselineName(name: string): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* Visual-snapshot baseline names as a Skyramp test passes them to
|
|
5
|
+
* `toHaveScreenshot()` — e.g. `page-001.png` — and as `skyramp_actions` and
|
|
6
|
+
* `skyramp_execute_test` accept them in `rebaselineSnapshots` (SKYR-4298).
|
|
7
|
+
*
|
|
8
|
+
* Bare filename only: no path separators, and no commas or whitespace, because the
|
|
9
|
+
* list is joined with commas into `SKYRAMP_UPDATE_SNAPSHOTS` and split again in the
|
|
10
|
+
* executor, where `a,b.png` would read as two baselines. One definition shared by
|
|
11
|
+
* both tools so they can never drift apart.
|
|
12
|
+
*/
|
|
13
|
+
export const REBASELINE_SNAPSHOT_NAME_RE = /^[^/\\,\s]+\.png$/i;
|
|
14
|
+
export const rebaselineSnapshotNameSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.trim()
|
|
17
|
+
.regex(REBASELINE_SNAPSHOT_NAME_RE, {
|
|
18
|
+
message: 'rebaselineSnapshots entries must be bare .png filenames as passed to toHaveScreenshot(), e.g. "page-001.png" — no path, commas, or whitespace',
|
|
19
|
+
});
|
|
20
|
+
/** `page-001.png` → `page-001`. */
|
|
21
|
+
export function baselineStem(name) {
|
|
22
|
+
return path.basename(name).replace(/\.png$/i, "");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Playwright's default snapshot layout, which is what the executor produces: the
|
|
26
|
+
* baseline for `<stem>.png` lives in `<spec>-snapshots/` as `<stem>-<project>-<platform>.png`
|
|
27
|
+
* (or `<stem>-<platform>.png` with no project, or `<stem>.png` under a custom
|
|
28
|
+
* `snapshotPathTemplate`). Safe to assume here because TestExecutionService mounts
|
|
29
|
+
* its own generated Playwright config over any the repo carries, so a repo cannot
|
|
30
|
+
* relocate its snapshots out from under this rule.
|
|
31
|
+
*
|
|
32
|
+
* The executor's generated config declares no `projects`, but runner.sh passes
|
|
33
|
+
* `--browser=chromium`, so Playwright names the implicit project "chromium" and a
|
|
34
|
+
* refresh writes `<stem>-chromium-linux.png` (observed end to end on demoshop).
|
|
35
|
+
* Baselines the Skyramp executor produced therefore round-trip exactly. A baseline
|
|
36
|
+
* a repo's own CI wrote under another project name (`<stem>-Desktop-Chrome-linux.png`)
|
|
37
|
+
* is a different file: the executor's refresh adds its own and leaves that one as
|
|
38
|
+
* it was — the tool reports what it rewrote, and the repo's own suite is out of
|
|
39
|
+
* scope for skyramp_execute_test (which never runs it).
|
|
40
|
+
*
|
|
41
|
+
* Anchored so a longer baseline cannot vouch for a shorter one: `page-001-wide-chromium-linux.png`
|
|
42
|
+
* belongs to `page-001-wide.png` and must not satisfy a claim on `page-001.png`.
|
|
43
|
+
*/
|
|
44
|
+
const SNAPSHOT_PLATFORMS = "(?:darwin|linux|win32)";
|
|
45
|
+
export function baselineFileMatchesStem(fileBasename, stem) {
|
|
46
|
+
// A degenerate stem ("" or dots only, from a name like "..png") would match a wide
|
|
47
|
+
// set of files; nothing legitimate has one.
|
|
48
|
+
if (!stem || /^\.+$/.test(stem))
|
|
49
|
+
return false;
|
|
50
|
+
const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
const re = new RegExp(`^${escaped}(?:-[^-]+-${SNAPSHOT_PLATFORMS}|-${SNAPSHOT_PLATFORMS})?\\.png$`, "i");
|
|
52
|
+
return re.test(fileBasename);
|
|
53
|
+
}
|
|
54
|
+
/** The directory Playwright keeps a spec's baselines in under the default layout. */
|
|
55
|
+
export function snapshotDirFor(specFile) {
|
|
56
|
+
return `${specFile}-snapshots`;
|
|
57
|
+
}
|
|
58
|
+
/** Looks like an on-disk baseline filename (`page-001-chromium-linux.png`) rather than the
|
|
59
|
+
* name the test passes (`page-001.png`) — the likeliest wrong input, and one the name
|
|
60
|
+
* schema accepts. Advisory only: `hero-linux.png` is also a perfectly valid name a test
|
|
61
|
+
* can pass, so callers warn and keep the entry; execution authorizes it against the
|
|
62
|
+
* verdict and reports whether anything was actually rewritten. */
|
|
63
|
+
export function looksLikeOnDiskBaselineName(name) {
|
|
64
|
+
return /-(?:[^-]+-)?(?:darwin|linux|win32)\.png$/i.test(name);
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RemovedUiElement {
|
|
2
|
+
/** Lower-cased identifying attribute: `data-testid`, `data-cy`, `data-qa`, `data-section`, … */
|
|
3
|
+
attribute: string;
|
|
4
|
+
/** Its literal value. `attribute` + `value` is unique across the result. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Repo-relative frontend file the diff removed it from. */
|
|
7
|
+
file: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CollectRemovedUiElementsInput {
|
|
10
|
+
changedFrontendFiles: string[];
|
|
11
|
+
/** Files with `+++ /dev/null` in the diff — their page is gone, no guard. */
|
|
12
|
+
deletedFiles: string[];
|
|
13
|
+
/** Per-file diff slices for the WHOLE diff keyed by repo-relative path (see diffBlocksByPath). */
|
|
14
|
+
diffByFile: Map<string, string>;
|
|
15
|
+
/** Absolute paths of discovered existing tests (skyramp + relevant external). */
|
|
16
|
+
existingTestFiles: string[];
|
|
17
|
+
/** True when test discovery threw — the existing-test check cannot run, so report nothing. */
|
|
18
|
+
discoveryFailed?: boolean;
|
|
19
|
+
/** Injectable for tests; defaults to a tolerant fs read that logs failures. */
|
|
20
|
+
readFile?: (absPath: string) => string | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare function collectRemovedUiElements(input: CollectRemovedUiElementsInput): RemovedUiElement[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removed UI elements (SKYR-4305) — grounding for the removal guard.
|
|
3
|
+
*
|
|
4
|
+
* The recommendation prompt already requires a candidate for every element a PR
|
|
5
|
+
* removes ("the removal is what this PR ships, so the removal is what gets tested").
|
|
6
|
+
* A blueprint can never contain a removed element, so this module supplies what the
|
|
7
|
+
* agent otherwise has to guess: WHICH identifying `data-*` attributes (`data-testid`,
|
|
8
|
+
* `data-cy`, `data-qa`, `data-test-id`, or another static `data-*`) the diff removed.
|
|
9
|
+
* The agent finds the page they rendered on. `data-testid` is asserted with `getByTestId`; every
|
|
10
|
+
* other attribute with `page.locator('[<attribute>="<value>"]')`, so the guard never
|
|
11
|
+
* silently resolves against the wrong attribute. It drafts nothing and gates nothing — it reduces noise:
|
|
12
|
+
*
|
|
13
|
+
* - an id is reported only when its file survives the change (not in
|
|
14
|
+
* `deletedFiles`) — a deleted route/component is maintenance of the tests that
|
|
15
|
+
* covered it, not a page to assert on;
|
|
16
|
+
* - renames, moves and extractions to another file are excluded by
|
|
17
|
+
* `extractRemovedAttributes` (added values are collected across the WHOLE diff);
|
|
18
|
+
* - a value an existing discovered test already references is skipped — that test is
|
|
19
|
+
* the one to update or delete, a new guard would duplicate it. The match is on the
|
|
20
|
+
* VALUE regardless of attribute (a test naming `user-menu-row` as a testid also
|
|
21
|
+
* suppresses a `data-section="user-menu-row"` guard) — deliberate over-suppression,
|
|
22
|
+
* the safe direction, even though entries are keyed on attribute + value elsewhere. When discovery
|
|
23
|
+
* itself failed the list is empty and nothing is reported (fail-closed), because
|
|
24
|
+
* "no test references it" cannot be known;
|
|
25
|
+
*
|
|
26
|
+
* Every skip is logged at debug with its reason so a missing guard is diagnosable.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from "fs";
|
|
29
|
+
import { extractAddedAttributeValues, extractRemovedAttributes } from "./frontendSelectors.js";
|
|
30
|
+
import { escapeRegExp } from "./regex.js";
|
|
31
|
+
import { logger } from "./logger.js";
|
|
32
|
+
function defaultReadFile(absPath) {
|
|
33
|
+
try {
|
|
34
|
+
return fs.readFileSync(absPath, "utf8");
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger.warning("removedUiElements: could not read existing test (treated as not referencing any removed id)", {
|
|
38
|
+
file: absPath,
|
|
39
|
+
error: err instanceof Error ? err.message : String(err),
|
|
40
|
+
});
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whole-literal match: `order-total` must not be satisfied by `order-total-row`, and
|
|
46
|
+
* `foo:bar` not by `foo:bar:baz`. The identifier alphabet mirrors the extractor's
|
|
47
|
+
* value grammar (`[\w:-]`), so every character a value may contain is a
|
|
48
|
+
* non-boundary here.
|
|
49
|
+
*/
|
|
50
|
+
function referencesLiteral(content, value) {
|
|
51
|
+
return new RegExp(`(^|[^\\w:-])${escapeRegExp(value)}(?![\\w:-])`).test(content);
|
|
52
|
+
}
|
|
53
|
+
export function collectRemovedUiElements(input) {
|
|
54
|
+
if (input.discoveryFailed) {
|
|
55
|
+
logger.warning("removedUiElements: test discovery failed — cannot tell whether an existing test covers a removed id; reporting none");
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const readFile = input.readFile ?? defaultReadFile;
|
|
59
|
+
const deleted = new Set(input.deletedFiles);
|
|
60
|
+
const skip = (file, reason, extra = {}) => logger.debug(`removedUiElements: skipped ${file} — ${reason}`, extra);
|
|
61
|
+
// Added values across the ENTIRE diff, so an element extracted into another file is a
|
|
62
|
+
// move, not a removal.
|
|
63
|
+
const addedAcrossDiff = new Set();
|
|
64
|
+
for (const slice of input.diffByFile.values()) {
|
|
65
|
+
for (const v of extractAddedAttributeValues(slice))
|
|
66
|
+
addedAcrossDiff.add(v);
|
|
67
|
+
}
|
|
68
|
+
// Existing-test contents are read lazily and once: most PRs remove nothing.
|
|
69
|
+
let testContents;
|
|
70
|
+
const referencedByExistingTest = (value) => {
|
|
71
|
+
if (!testContents) {
|
|
72
|
+
testContents = input.existingTestFiles
|
|
73
|
+
.map((f) => readFile(f) ?? "")
|
|
74
|
+
.filter((c) => c.length > 0);
|
|
75
|
+
}
|
|
76
|
+
return testContents.some((c) => referencesLiteral(c, value));
|
|
77
|
+
};
|
|
78
|
+
const out = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const file of input.changedFrontendFiles) {
|
|
81
|
+
if (deleted.has(file)) {
|
|
82
|
+
skip(file, "file deleted by the diff (whole surface removed)");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const slice = input.diffByFile.get(file);
|
|
86
|
+
if (!slice) {
|
|
87
|
+
skip(file, "no diff slice");
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const attrs = extractRemovedAttributes(slice, addedAcrossDiff);
|
|
91
|
+
if (attrs.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
for (const { attribute, value } of attrs) {
|
|
94
|
+
const key = `${attribute}=${value}`;
|
|
95
|
+
if (seen.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
if (referencedByExistingTest(value)) {
|
|
98
|
+
skip(file, `${attribute}="${value}" already referenced by an existing test (maintenance, not a guard)`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
seen.add(key);
|
|
102
|
+
out.push({ attribute, value, file });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MaintenanceActionCore } from "../types/TestAnalysis.js";
|
|
2
2
|
/**
|
|
3
3
|
* Enumerate every path that differs from HEAD in the working tree — staged,
|
|
4
4
|
* unstaged, or untracked — as repo-relative paths, via `git status --porcelain`.
|
|
@@ -42,11 +42,7 @@ export interface UnchangedFileClaimsInput {
|
|
|
42
42
|
}[];
|
|
43
43
|
/** Maintenance verdicts (server-derived from skyramp_actions). `pomFile`, when set, is
|
|
44
44
|
* the file the edit actually lands in — see the UPDATE rule below. */
|
|
45
|
-
verdicts:
|
|
46
|
-
action: DriftAction;
|
|
47
|
-
testFilePath: string;
|
|
48
|
-
pomFile?: string;
|
|
49
|
-
}[];
|
|
45
|
+
verdicts: Pick<MaintenanceActionCore, "action" | "testFilePath" | "pomFile" | "rebaselineSnapshots" | "rebaselineOnly">[];
|
|
50
46
|
/** owner/repo of the primary repo — used to exempt cross-repo newTest claims
|
|
51
47
|
* whose files live in a different checkout. */
|
|
52
48
|
primaryRepository?: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
3
4
|
import { execFile } from "child_process";
|
|
4
5
|
import { promisify } from "util";
|
|
5
6
|
import { testFileMatches } from "./utils.js";
|
|
6
7
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
8
|
+
import { baselineFileMatchesStem, baselineStem } from "./rebaselineSnapshots.js";
|
|
7
9
|
const execFileAsync = promisify(execFile);
|
|
8
10
|
/**
|
|
9
11
|
* Enumerate every path that differs from HEAD in the working tree — staged,
|
|
@@ -137,6 +139,40 @@ export function findUnchangedFileClaims(input) {
|
|
|
137
139
|
}
|
|
138
140
|
return changedFiles.some((c) => testFileMatches(c, candidate));
|
|
139
141
|
};
|
|
142
|
+
// A visual-baseline UPDATE (SKYR-4298) edits no source: SmartPlaywright rewrites
|
|
143
|
+
// the PNG Playwright keeps beside the spec, `<spec>-snapshots/<name>-<project>-
|
|
144
|
+
// <platform>.png` (the default layout — see baselineFileMatchesStem for why that
|
|
145
|
+
// is safe to assume). Backed when one of the named baselines changed there AND
|
|
146
|
+
// still exists: `git status` lists a deleted file as changed too, and a destroyed
|
|
147
|
+
// baseline is the opposite of a refreshed one. Scoped to the spec's own snapshot
|
|
148
|
+
// directory so a refreshed baseline of some other spec cannot vouch for this one.
|
|
149
|
+
const isBackedBySnapshot = (specPath, baselines) => {
|
|
150
|
+
let specRel = specPath;
|
|
151
|
+
if (path.isAbsolute(specPath)) {
|
|
152
|
+
const rel = path.relative(repoRoot, specPath);
|
|
153
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
154
|
+
return true;
|
|
155
|
+
specRel = rel;
|
|
156
|
+
}
|
|
157
|
+
// git reports paths with "/" on every platform; path.relative uses the host
|
|
158
|
+
// separator, so a Windows run would otherwise never match its own snapshot dir.
|
|
159
|
+
specRel = specRel.split(path.sep).join("/");
|
|
160
|
+
const snapshotDir = `${specRel}-snapshots/`;
|
|
161
|
+
const stems = baselines.map(baselineStem).filter(Boolean);
|
|
162
|
+
return changedFiles.some((c) => {
|
|
163
|
+
// Same anchoring as testFileMatches: a path-qualified spec must match as a
|
|
164
|
+
// path segment; a bare basename can only match by directory basename.
|
|
165
|
+
const inDir = specRel.includes("/")
|
|
166
|
+
? c.startsWith(snapshotDir) || c.includes("/" + snapshotDir)
|
|
167
|
+
: path.basename(path.dirname(c)) === snapshotDir.slice(0, -1);
|
|
168
|
+
if (!inDir)
|
|
169
|
+
return false;
|
|
170
|
+
const base = path.basename(c);
|
|
171
|
+
if (!stems.some((stem) => baselineFileMatchesStem(base, stem)))
|
|
172
|
+
return false;
|
|
173
|
+
return existsSync(path.resolve(repoRoot, c));
|
|
174
|
+
});
|
|
175
|
+
};
|
|
140
176
|
// A plan-only run declares its GENERATE list instead of writing files, so
|
|
141
177
|
// skip the claim scan wholesale rather than per-claim: checking it rejects
|
|
142
178
|
// every declaration the guard can see, and the ones it CANNOT see (cross-repo
|
|
@@ -165,9 +201,32 @@ export function findUnchangedFileClaims(input) {
|
|
|
165
201
|
// The edit target, not the spec: for a POM-based test the page object is what
|
|
166
202
|
// gets patched while testFilePath stays the spec (SKYR-4129).
|
|
167
203
|
const editTarget = v.pomFile ?? v.testFilePath;
|
|
168
|
-
|
|
169
|
-
|
|
204
|
+
// Baseline refresh (SKYR-4298): the verdict promises a replaced PNG next to the
|
|
205
|
+
// spec, and the report says "visual baseline refreshed" on its strength. Two rules:
|
|
206
|
+
// - The PNG must have been rewritten. An edit to the spec does not stand in for
|
|
207
|
+
// it, otherwise a run that forgot rebaselineSnapshots (or loosened
|
|
208
|
+
// maxDiffPixelRatio instead) would report a refresh that never happened.
|
|
209
|
+
// - Listing a baseline never exempts the edit the same verdict claimed. Unless the
|
|
210
|
+
// verdict is rebaselineOnly (no updateInstructions), the edit target — the POM
|
|
211
|
+
// when named, else the spec — still needs its own working-tree change; a
|
|
212
|
+
// baseline attached to a selector-rename UPDATE cannot wave the rename through.
|
|
213
|
+
// A plan-only run executes nothing, so no PNG can be rewritten there; the edit
|
|
214
|
+
// half is still verified, as for every other UPDATE (SKYR-4250).
|
|
215
|
+
const baselines = v.rebaselineSnapshots ?? [];
|
|
216
|
+
if (baselines.length > 0) {
|
|
217
|
+
const editOk = v.rebaselineOnly === true || isBacked(editTarget);
|
|
218
|
+
const pngOk = planOnly || isBackedBySnapshot(v.testFilePath, baselines);
|
|
219
|
+
if (!pngOk) {
|
|
220
|
+
unbacked.push(`testMaintenance ${v.action} claims a refreshed visual baseline (${baselines.join(", ")}) for "${path.basename(v.testFilePath)}", but no matching PNG was rewritten under its -snapshots directory — the final skyramp_execute_test run must pass rebaselineSnapshots and report it as refreshed; editing the spec does not stand in for the refresh`);
|
|
221
|
+
}
|
|
222
|
+
if (!editOk) {
|
|
223
|
+
unbacked.push(`testMaintenance ${v.action} claims an edit to "${path.basename(editTarget)}" alongside a baseline refresh, but that file has no working-tree change — listing a baseline does not exempt the edit`);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
170
226
|
}
|
|
227
|
+
if (isBacked(editTarget))
|
|
228
|
+
continue;
|
|
229
|
+
unbacked.push(`testMaintenance ${v.action} claims an edit to "${path.basename(editTarget)}", but that file has no working-tree change`);
|
|
171
230
|
}
|
|
172
231
|
return unbacked;
|
|
173
232
|
}
|