@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.
Files changed (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -0,0 +1,17 @@
1
+ import { Verifier } from "../types.js";
2
+ /** A path that leaves the checkout, in any of its spellings. Callers pass a
3
+ * trimmed path: " ../routes.rb" splits to [" ..", "routes.rb"], so no segment
4
+ * equals ".." and one leading space would carry it through. */
5
+ export declare function escapesRepo(file: string): boolean;
6
+ export declare const NOT_SOURCE_DIR: RegExp;
7
+ /** What plainly cannot define a route. A DENY list on purpose: an allowlist would
8
+ * rebuild the framework wall this verifier removes. When in doubt ALLOW — a false
9
+ * accept costs one unfounded citation, a false reject a whole framework. */
10
+ export declare const NOT_A_ROUTE_FILE: RegExp;
11
+ /** Lockfiles, including the two that end in an allowed extension. */
12
+ export declare const LOCKFILE: RegExp;
13
+ /** Verifier 2. Every step a planned test makes cites the repository file that
14
+ * declares it, and the citation is the WHOLE check — the route catalog and the
15
+ * diff's routes it replaced were framework parsers, blind to GraphQL, Thrift and
16
+ * gRPC. A `ui` or `e2e` planned test is exempt whole; a checked one, per step. */
17
+ export declare const endpointGrounded: Verifier;
@@ -0,0 +1,128 @@
1
+ import { isUIPlannedTest } from "../subjectStep.js";
2
+ import { ENDPOINT_GROUNDED_CONTRACT } from "../verifierContracts.js";
3
+ import { isTestPath } from "../../utils/testFileClassification.js";
4
+ /** A path that leaves the checkout, in any of its spellings. Callers pass a
5
+ * trimmed path: " ../routes.rb" splits to [" ..", "routes.rb"], so no segment
6
+ * equals ".." and one leading space would carry it through. */
7
+ export function escapesRepo(file) {
8
+ // POSIX absolute, a Windows drive, a bare leading backslash, and a UNC share.
9
+ if (file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(file))
10
+ return true;
11
+ // A URL, which both citation describes promise is not a citation: `http://host/x`
12
+ // has no leading slash and no `..`, so without this it resolved against the
13
+ // checkout. A lone colon is NOT enough (`orders:batchGet.rb` is a legal name), so
14
+ // a scheme counts only with a leading slash, a nested URL, or a non-file scheme.
15
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):([\s\S]*)$/.exec(file);
16
+ if (scheme) {
17
+ const [, name, rest] = scheme;
18
+ if (rest.startsWith("/") || rest.includes("://") || /^(file|data|javascript|jar|blob)$/i.test(name))
19
+ return true;
20
+ }
21
+ return file.split(/[\\/]/).some((segment) => segment === "..");
22
+ }
23
+ export const NOT_SOURCE_DIR = /(^|[\\/])(node_modules|vendor|dist|build|\.next|target)([\\/]|$)/i;
24
+ /** What plainly cannot define a route. A DENY list on purpose: an allowlist would
25
+ * rebuild the framework wall this verifier removes. When in doubt ALLOW — a false
26
+ * accept costs one unfounded citation, a false reject a whole framework. */
27
+ export const NOT_A_ROUTE_FILE = /\.(md|markdown|rst|adoc|txt|log|csv|tsv|pdf|png|jpe?g|gif|svg|ico|webp|bmp|mp4|mov|zip|gz|tgz|tar|woff2?|ttf|eot|snap)$/i;
28
+ /** Lockfiles, including the two that end in an allowed extension. */
29
+ export const LOCKFILE = /(^|[\\/])([^\\/]*[-.]lock\.(json|ya?ml)|[^\\/]*\.lock)$/i;
30
+ /** Whether a cited file could plausibly define a route. DISQUALIFICATION, not
31
+ * proof: it does NOT look inside, because frameworks assemble paths from scopes
32
+ * and prefixes. A string check only — symlinks are the file reader's job. */
33
+ function citationDisqualified(rawFile, ctx) {
34
+ // Normalise once, before any check: every check below reads this value, so a
35
+ // check that ran on the raw string could disagree with the one before it.
36
+ const file = String(rawFile ?? "").trim();
37
+ if (!file)
38
+ return "no file cited";
39
+ if (escapesRepo(file))
40
+ return `cited path "${file}" points outside the repository`;
41
+ if (NOT_SOURCE_DIR.test(file))
42
+ return `cited file ${file} is vendored or built output, not source`;
43
+ if (isTestPath(file))
44
+ return `cited file ${file} is a test file, not the source that defines the route`;
45
+ if (LOCKFILE.test(file))
46
+ return `cited file ${file} is a lockfile, not a file that can define a route`;
47
+ if (NOT_A_ROUTE_FILE.test(file))
48
+ return `cited file ${file} is documentation or a binary, not a file that can define a route`;
49
+ if (!ctx.citedFileExists(file))
50
+ return `cited file not found: ${file}`;
51
+ return undefined;
52
+ }
53
+ /** How the objection reads when the routes have no citation, when their
54
+ * citations are implausible, and when the planned test has some of each. */
55
+ function describeObjection(uncited, disqualified) {
56
+ if (disqualified === 0)
57
+ return ENDPOINT_GROUNDED_CONTRACT.objections.uncited;
58
+ if (uncited === 0)
59
+ return ENDPOINT_GROUNDED_CONTRACT.objections.disqualifiedCitation;
60
+ return ENDPOINT_GROUNDED_CONTRACT.objections.uncitedAndDisqualified;
61
+ }
62
+ /** Verifier 2. Every step a planned test makes cites the repository file that
63
+ * declares it, and the citation is the WHOLE check — the route catalog and the
64
+ * diff's routes it replaced were framework parsers, blind to GraphQL, Thrift and
65
+ * gRPC. A `ui` or `e2e` planned test is exempt whole; a checked one, per step. */
66
+ export const endpointGrounded = {
67
+ name: "endpointGrounded",
68
+ run(registration, ctx) {
69
+ const objections = [];
70
+ for (const plannedTest of registration.plannedTests) {
71
+ if (isUIPlannedTest(plannedTest))
72
+ continue;
73
+ const evidence = plannedTest.declarations?.routes ?? [];
74
+ const ungrounded = [];
75
+ let uncited = 0;
76
+ let disqualified = 0;
77
+ for (const step of plannedTest.scenario?.steps ?? []) {
78
+ // A step naming no path calls nothing there is a file to cite for — a wait,
79
+ // a pause, a setup line. The verb is deliberately NOT read: requiring one
80
+ // made a Thrift or GraphQL call look like a click and skip this check.
81
+ if (!String(step?.path ?? "").trim())
82
+ continue;
83
+ const key = `${String(step?.method ?? "ANY").trim() || "ANY"} ${String(step?.path).trim()}`;
84
+ // Named by the step's own `order`, so nothing here compares two spellings
85
+ // of one route. The number must be a number: a string `"2"` names no step.
86
+ const cited = evidence.filter((e) => typeof e?.step === "number" && e.step === (step?.order ?? 0));
87
+ if (cited.length === 0) {
88
+ uncited += 1;
89
+ pushOnce(ungrounded, `${key} (no file cited)`);
90
+ continue;
91
+ }
92
+ // ANY one plausible citation grounds the route. A planned test that cites
93
+ // two files for one route must not fail on the weaker of them.
94
+ const reasons = [];
95
+ let grounded = false;
96
+ for (const citation of cited) {
97
+ const reason = citationDisqualified(citation.file, ctx);
98
+ if (reason === undefined) {
99
+ grounded = true;
100
+ break;
101
+ }
102
+ reasons.push(reason);
103
+ }
104
+ if (grounded)
105
+ continue;
106
+ disqualified += 1;
107
+ pushOnce(ungrounded, `${key} (${reasons[0]})`);
108
+ }
109
+ if (ungrounded.length > 0) {
110
+ const { message, suggestion } = describeObjection(uncited, disqualified);
111
+ objections.push({
112
+ objectionId: `endpointGrounded:${plannedTest.plannedTestId}`,
113
+ verifier: "endpointGrounded",
114
+ plannedTestId: plannedTest.plannedTestId,
115
+ message,
116
+ evidence: `ungrounded: ${ungrounded.join("; ")}`,
117
+ suggestion,
118
+ });
119
+ continue;
120
+ }
121
+ }
122
+ return objections;
123
+ },
124
+ };
125
+ function pushOnce(list, note) {
126
+ if (!list.includes(note))
127
+ list.push(note);
128
+ }
@@ -0,0 +1,6 @@
1
+ import { Verifier } from "../types.js";
2
+ /** Verifier 6. The existing tests a planned test says it read are really in the
3
+ * checkout, and nothing more: deciding that one already covers the planned test came
4
+ * from route-parsed `METHOD::resource` keys, wrong in both directions. `differsBy`
5
+ * is recorded, never judged, and citing nothing raises nothing. */
6
+ export declare const existingCoverage: Verifier;
@@ -0,0 +1,51 @@
1
+ import { escapesRepo } from "./endpointGrounded.js";
2
+ import { EXISTING_COVERAGE_CONTRACT } from "../verifierContracts.js";
3
+ /** Verifier 6. The existing tests a planned test says it read are really in the
4
+ * checkout, and nothing more: deciding that one already covers the planned test came
5
+ * from route-parsed `METHOD::resource` keys, wrong in both directions. `differsBy`
6
+ * is recorded, never judged, and citing nothing raises nothing. */
7
+ export const existingCoverage = {
8
+ name: "existingCoverage",
9
+ run(registration, ctx) {
10
+ const objections = [];
11
+ for (const plannedTest of registration.plannedTests) {
12
+ // The plan is read off disk, so this is an array only on the validated
13
+ // path. A throw here would reach the agent as `:crashed`, hiding the real
14
+ // objection instead of raising it.
15
+ const cited = plannedTest?.declarations?.existingTests;
16
+ if (!Array.isArray(cited))
17
+ continue;
18
+ const unreadable = [];
19
+ for (const entry of cited) {
20
+ const file = String(entry?.file ?? "").trim();
21
+ if (!file) {
22
+ pushOnce(unreadable, "an entry names no file");
23
+ continue;
24
+ }
25
+ // Confinement first: `citedFileExists` resolves inside the checkout, and
26
+ // a path that leaves it is not a citation of anything this run holds.
27
+ if (escapesRepo(file)) {
28
+ pushOnce(unreadable, `${file} (points outside the repository)`);
29
+ continue;
30
+ }
31
+ if (!ctx.citedFileExists(file))
32
+ pushOnce(unreadable, `${file} (not found)`);
33
+ }
34
+ if (unreadable.length === 0)
35
+ continue;
36
+ objections.push({
37
+ objectionId: `existingCoverage:${plannedTest.plannedTestId}`,
38
+ verifier: "existingCoverage",
39
+ plannedTestId: plannedTest.plannedTestId,
40
+ message: EXISTING_COVERAGE_CONTRACT.objections.citedTestMissing.message,
41
+ evidence: `cited existing tests not in the checkout: ${unreadable.join("; ")}`,
42
+ suggestion: EXISTING_COVERAGE_CONTRACT.objections.citedTestMissing.suggestion,
43
+ });
44
+ }
45
+ return objections;
46
+ },
47
+ };
48
+ function pushOnce(list, note) {
49
+ if (!list.includes(note))
50
+ list.push(note);
51
+ }
@@ -0,0 +1,31 @@
1
+ import { Objection, Verifier } from "../types.js";
2
+ import { Plan } from "../registerPlan.js";
3
+ /** One test's outcome, as the execution stage reports it. */
4
+ export interface ExecutionOutcome {
5
+ plannedTestId: string;
6
+ passed: boolean;
7
+ /** The failure text, when it failed. */
8
+ failureDetail?: string;
9
+ /** Whether the test reached the behaviour it was written for before it failed, as
10
+ * the agent states it. ABSENT means unstated and raises nothing: inferring it
11
+ * from the failure text meant an allowlist of blocker phrases over free prose. */
12
+ reachedBehavior?: boolean;
13
+ }
14
+ /** One `issuesFound` entry, as much of it as this check reads. */
15
+ export interface ReportedBlocker {
16
+ severity?: string;
17
+ description?: string;
18
+ sourceFile?: string;
19
+ }
20
+ /** Verifier 4, post-execution half: separates a failure that caught the bug from one
21
+ * that fell over before reaching it. Joined by `plannedTestId`, never by array
22
+ * position, and a planned test with no result produces nothing. The ids carry an
23
+ * `executed` segment so one answer cannot close both halves. `issues` is the
24
+ * report's own `issuesFound`: when one of them names the blocked test's route,
25
+ * the objection quotes it so the answer is one sentence. */
26
+ export declare function checkExpectedOutcomeAfterExecution(plan: Plan, results: ExecutionOutcome[], issues?: ReportedBlocker[]): Objection[];
27
+ /** Verifier 4, plan time. STRUCTURAL ONLY: a planned test in a bug-catching category
28
+ * that expects to pass on the current app is contradicting itself. Whether the
29
+ * `asserts` prose describes the fixed behaviour or the broken one needs a model,
30
+ * and string matching cannot stand in for it — do not add one. */
31
+ export declare const expectedOutcomeAtPlanTime: Verifier;
@@ -0,0 +1,105 @@
1
+ import { categoryExpectsRed } from "../../types/TestRecommendation.js";
2
+ import { fillPlaceholders, EXPECTED_OUTCOME_CONTRACT } from "../verifierContracts.js";
3
+ import { subjectStep } from "../subjectStep.js";
4
+ /** The severities that count as the run's own reported defect. */
5
+ const BLOCKER_FLOOR = ["critical", "high"];
6
+ /** The reported issue that names this test's own route, if one does. Containment
7
+ * on the path and nothing more: the aim is to hand the agent the sentence it
8
+ * would otherwise retype, not to decide whether the issue really is the cause. */
9
+ function blockingIssue(plannedTest, issues) {
10
+ const path = String(subjectStep(plannedTest)?.path ?? "").trim();
11
+ if (!path)
12
+ return undefined;
13
+ for (const issue of issues) {
14
+ if (!BLOCKER_FLOOR.includes(String(issue?.severity ?? "").trim().toLowerCase()))
15
+ continue;
16
+ const description = String(issue?.description ?? "");
17
+ const sourceFile = String(issue?.sourceFile ?? "");
18
+ if (!description.includes(path) && !sourceFile.includes(path))
19
+ continue;
20
+ const text = description.trim() || sourceFile.trim();
21
+ if (text)
22
+ return text.slice(0, 100);
23
+ }
24
+ return undefined;
25
+ }
26
+ /** Verifier 4, post-execution half: separates a failure that caught the bug from one
27
+ * that fell over before reaching it. Joined by `plannedTestId`, never by array
28
+ * position, and a planned test with no result produces nothing. The ids carry an
29
+ * `executed` segment so one answer cannot close both halves. `issues` is the
30
+ * report's own `issuesFound`: when one of them names the blocked test's route,
31
+ * the objection quotes it so the answer is one sentence. */
32
+ export function checkExpectedOutcomeAfterExecution(plan, results, issues = []) {
33
+ const byId = new Map(results.map((r) => [r.plannedTestId, r]));
34
+ const objections = [];
35
+ for (const plannedTest of plan.plannedTests) {
36
+ const result = byId.get(plannedTest.plannedTestId);
37
+ if (!result)
38
+ continue;
39
+ const declared = plannedTest.declarations?.expected?.outcome;
40
+ if (declared === "fail" && result.passed) {
41
+ objections.push({
42
+ objectionId: `expectedOutcome:executed:${plannedTest.plannedTestId}`,
43
+ verifier: "expectedOutcome",
44
+ plannedTestId: plannedTest.plannedTestId,
45
+ message: "This test was declared should-fail and passed. Either the bug is not there or the test does not reach it.",
46
+ evidence: `declared fail, passed`,
47
+ suggestion: "Say which it is: the behaviour is already correct, so the objection does not stand, or the test misses the path that carries the defect and needs to target it.",
48
+ });
49
+ continue;
50
+ }
51
+ if (declared === "pass" && !result.passed) {
52
+ objections.push({
53
+ objectionId: `expectedOutcome:executed:${plannedTest.plannedTestId}`,
54
+ verifier: "expectedOutcome",
55
+ plannedTestId: plannedTest.plannedTestId,
56
+ message: "This test was declared should-pass and failed. Say whether the failure is the bug or the test.",
57
+ evidence: `declared pass, failed: ${result.failureDetail ?? "no detail"}`,
58
+ suggestion: "If the failure is a real defect, report it as one. If the test itself is wrong, fix the test rather than reporting the failure.",
59
+ });
60
+ continue;
61
+ }
62
+ // Stated by the agent, not inferred: a 404 the test asked for and a 404 that
63
+ // stopped it read the same in the failure text, and only whoever wrote the
64
+ // test knows which it was. A row that leaves the field out raises nothing.
65
+ if (declared === "fail" && !result.passed && result.reachedBehavior === false) {
66
+ const blocker = blockingIssue(plannedTest, issues);
67
+ objections.push({
68
+ objectionId: `expectedOutcome:executed:${plannedTest.plannedTestId}`,
69
+ verifier: "expectedOutcome",
70
+ plannedTestId: plannedTest.plannedTestId,
71
+ message: EXPECTED_OUTCOME_CONTRACT.objections.executed.message,
72
+ evidence: blocker ? `blocked by reported issue: ${blocker}` : (result.failureDetail ?? ""),
73
+ suggestion: EXPECTED_OUTCOME_CONTRACT.objections.executed.suggestion,
74
+ });
75
+ }
76
+ }
77
+ return objections;
78
+ }
79
+ /** Verifier 4, plan time. STRUCTURAL ONLY: a planned test in a bug-catching category
80
+ * that expects to pass on the current app is contradicting itself. Whether the
81
+ * `asserts` prose describes the fixed behaviour or the broken one needs a model,
82
+ * and string matching cannot stand in for it — do not add one. */
83
+ export const expectedOutcomeAtPlanTime = {
84
+ name: "expectedOutcome",
85
+ run(registration, _ctx) {
86
+ const objections = [];
87
+ for (const plannedTest of registration.plannedTests) {
88
+ // Compared normalised, reported as written: the agent has to recognise the
89
+ // value it sent in the objection.
90
+ const category = String(plannedTest.scenario?.category ?? "");
91
+ const declaredOutcome = plannedTest.declarations?.expected?.outcome;
92
+ if (declaredOutcome !== "pass" || !categoryExpectsRed(category))
93
+ continue;
94
+ objections.push({
95
+ objectionId: `expectedOutcome:${plannedTest.plannedTestId}`,
96
+ verifier: "expectedOutcome",
97
+ plannedTestId: plannedTest.plannedTestId,
98
+ message: fillPlaceholders(EXPECTED_OUTCOME_CONTRACT.objections.passInShouldFailCategory.message, { category }),
99
+ evidence: `category ${category}, declared pass: "${plannedTest.declarations?.expected?.why ?? ""}"`,
100
+ suggestion: EXPECTED_OUTCOME_CONTRACT.objections.passInShouldFailCategory.suggestion,
101
+ });
102
+ }
103
+ return objections;
104
+ },
105
+ };
@@ -0,0 +1,2 @@
1
+ import { Verifier } from "../types.js";
2
+ export declare const removedElementGuarded: Verifier;
@@ -0,0 +1,57 @@
1
+ /** Verifier 9. Every element the diff removed needs a planned test that says it is
2
+ * gone.
3
+ *
4
+ * v1 solved this in the server: SKYR-4339 resolved each removed element to a page
5
+ * through the import graph and, when that found none, through a root fallback —
6
+ * the fallback existing only so an element whose page could not be resolved still
7
+ * produced a guard instead of being dropped. v2 took page attribution out of the
8
+ * server, so the same protection has to read the plan instead: the run is free to
9
+ * decide which page the element belongs on, and is not free to leave it out.
10
+ *
11
+ * This is the direction `uiElementGrounded` does not check. That one asks whether
12
+ * a declared element is real; this one asks whether a real removal was declared. */
13
+ const text = (value) => (typeof value === "string" ? value.trim() : "");
14
+ /** How a plan names an element. `testId` is the one field that carries the value
15
+ * for a `data-testid`; a guard on `data-cy` or `data-qa` reaches the plan with
16
+ * `testId` null, so the value is looked for in the prose the test states too. */
17
+ function namesElement(plannedTest, value) {
18
+ const declared = plannedTest?.declarations;
19
+ const items = Array.isArray(declared?.elements?.items) ? declared.elements.items : [];
20
+ if (items.some((item) => text(item?.testId) === value || text(item?.stableId) === value))
21
+ return true;
22
+ return [text(declared?.asserts), text(plannedTest?.plannedTestId)].some((prose) => prose.includes(value));
23
+ }
24
+ /** `attribute=value` identifies an element across the run, so the objection id is
25
+ * the same string on every registration whatever order the list arrives in. */
26
+ const objectionIdFor = (element) => `removedElementGuarded:${text(element?.attribute)}=${text(element?.value)}`;
27
+ export const removedElementGuarded = {
28
+ name: "removedElementGuarded",
29
+ run(registration, ctx) {
30
+ const removed = Array.isArray(ctx?.removedUiElements) ? ctx.removedUiElements : [];
31
+ if (removed.length === 0)
32
+ return [];
33
+ const plannedTests = Array.isArray(registration?.plannedTests) ? registration.plannedTests : [];
34
+ const objections = [];
35
+ const seen = new Set();
36
+ for (const element of removed) {
37
+ const value = text(element?.value);
38
+ const attribute = text(element?.attribute);
39
+ if (!value || !attribute)
40
+ continue;
41
+ const objectionId = objectionIdFor(element);
42
+ if (seen.has(objectionId))
43
+ continue;
44
+ seen.add(objectionId);
45
+ if (plannedTests.some((plannedTest) => namesElement(plannedTest, value)))
46
+ continue;
47
+ objections.push({
48
+ objectionId,
49
+ verifier: "removedElementGuarded",
50
+ message: `The diff removes \`${attribute}="${value}"\` and no planned test says it is gone.`,
51
+ evidence: `removed from ${text(element?.file) || "a changed frontend file"}; no planned test names ${value}`,
52
+ suggestion: "Plan a UI test on the page that still renders, asserting this element is absent, and name it in that test's elements. Removing it is what this pull request ships, so the removal is what gets tested. If the diff shows it still rendering under a new identifier it is a rename, not a removal — say so as the answer.",
53
+ });
54
+ }
55
+ return objections;
56
+ },
57
+ };
@@ -0,0 +1,26 @@
1
+ import { Objection } from "../types.js";
2
+ import { Plan } from "../registerPlan.js";
3
+ /** One test as the report describes it. */
4
+ export interface ReportedTest {
5
+ plannedTestId?: string;
6
+ fileName: string;
7
+ category?: string;
8
+ }
9
+ /** One entry of the report's `issuesFound`. Every field optional: the caller
10
+ * reads them off a submitted report, where only the schema-validated path
11
+ * guarantees a shape. */
12
+ export interface ReportedIssue {
13
+ severity?: string;
14
+ /** The planned test this issue is about, when the report names one. */
15
+ plannedTestId?: string;
16
+ }
17
+ /** Verifier 9, post-execution. The category a test is REPORTED under is the one it
18
+ * was PLANNED under — a direct comparison, since both sides take the same category
19
+ * list. An objection, never a rejection: the tests exist, and a category changed
20
+ * on purpose is answerable. */
21
+ export declare function checkReportedCategoryMatchesPlan(plan: Plan, delivered: ReportedTest[]): Objection[];
22
+ /** Verifier 10, post-execution. A requirement conflict is reported as an issue at
23
+ * `critical` or `high`, read plan-side, one objection per run. It asks only that
24
+ * SOMETHING met the floor: the only field both sides share is a path, and the
25
+ * route's file and the wrong code's file differ by design in a layered app. */
26
+ export declare function checkRequirementConflictReported(plan: Plan, delivered: ReportedTest[], issues: ReportedIssue[]): Objection[];
@@ -0,0 +1,84 @@
1
+ import { normalizeCategory } from "../../types/TestRecommendation.js";
2
+ /** The severities a requirement conflict is allowed to be reported at. */
3
+ const REQUIREMENT_CONFLICT_FLOOR = ["critical", "high"];
4
+ /** The planned planned test a reported test joins to, by id and never by name. */
5
+ function plannedTest(plan, test) {
6
+ const id = test.plannedTestId?.trim();
7
+ if (!id)
8
+ return undefined;
9
+ return plan.plannedTests.find((plannedTest) => plannedTest.plannedTestId === id);
10
+ }
11
+ function plannedCategory(plan, test) {
12
+ return plannedTest(plan, test)?.scenario?.category;
13
+ }
14
+ /** Verifier 9, post-execution. The category a test is REPORTED under is the one it
15
+ * was PLANNED under — a direct comparison, since both sides take the same category
16
+ * list. An objection, never a rejection: the tests exist, and a category changed
17
+ * on purpose is answerable. */
18
+ export function checkReportedCategoryMatchesPlan(plan, delivered) {
19
+ const objections = [];
20
+ for (const test of delivered) {
21
+ const planned = plannedCategory(plan, test);
22
+ const reported = String(test.category ?? "").trim();
23
+ if (!planned || !reported)
24
+ continue;
25
+ if (reported === planned)
26
+ continue;
27
+ objections.push({
28
+ objectionId: `reportedCategory:${test.plannedTestId}`,
29
+ verifier: "reportedCategory",
30
+ plannedTestId: test.plannedTestId,
31
+ message: "This test is reported under a different category from the one it was planned under.",
32
+ evidence: `planned ${planned}, report says ${reported}`,
33
+ suggestion: "Say which category the test really belongs to. If the plan's was wrong, the report's category stands and this objection records the change; if the report's is a slip, correct it so the two agree.",
34
+ });
35
+ }
36
+ return objections;
37
+ }
38
+ /** Verifier 10, post-execution. A requirement conflict is reported as an issue at
39
+ * `critical` or `high`, read plan-side, one objection per run. It asks only that
40
+ * SOMETHING met the floor: the only field both sides share is a path, and the
41
+ * route's file and the wrong code's file differ by design in a layered app. */
42
+ export function checkRequirementConflictReported(plan, delivered, issues) {
43
+ // Normalised, not compared as written: `Requirement-Conflict` counts as red
44
+ // everywhere else and still escaped this severity floor.
45
+ const conflicts = delivered.filter((test) => normalizeCategory(plannedCategory(plan, test)) === "requirement_conflict");
46
+ if (conflicts.length === 0)
47
+ return [];
48
+ const severities = issues.map((issue) => String(issue?.severity ?? "").trim().toLowerCase());
49
+ const isAtFloor = (issue) => REQUIREMENT_CONFLICT_FLOOR.includes(String(issue?.severity ?? "").trim().toLowerCase());
50
+ const atFloor = issues.filter(isAtFloor);
51
+ if (atFloor.length === 0) {
52
+ const named = conflicts.map((test) => test.fileName).join(", ");
53
+ return [
54
+ {
55
+ objectionId: "requirementConflictReported:severity",
56
+ verifier: "requirementConflictReported",
57
+ message: "This run wrote a requirement-conflict test and reported no issue at critical or high.",
58
+ evidence: severities.length === 0
59
+ ? `${conflicts.length} requirement-conflict test(s): ${named}; issuesFound is empty`
60
+ : `${conflicts.length} requirement-conflict test(s): ${named}; issuesFound severities: ${severities.join(", ")}`,
61
+ suggestion: "Record the conflict in `issuesFound` at `critical` or `high`, quoting the requirement it contradicts — a stated requirement the code does not meet is not a medium. If the conflict turned out not to hold, say so here and drop the category from the test.",
62
+ },
63
+ ];
64
+ }
65
+ // Per CANDIDATE once the report names planned tests at all: a run-wide "some issue
66
+ // is severe" let an unrelated outage stand in for a conflict nobody wrote up. A
67
+ // report whose issues name no planned test keeps the run-level answer above.
68
+ if (!issues.some((issue) => String(issue?.plannedTestId ?? "").trim().length > 0))
69
+ return [];
70
+ const unnamed = conflicts.filter((test) => !atFloor.some((issue) => String(issue?.plannedTestId ?? "").trim() === String(test.plannedTestId ?? "").trim()
71
+ && String(test.plannedTestId ?? "").trim().length > 0));
72
+ if (unnamed.length === 0)
73
+ return [];
74
+ return [
75
+ {
76
+ objectionId: "requirementConflictReported:association",
77
+ verifier: "requirementConflictReported",
78
+ message: "A requirement-conflict test has no issue of its own at critical or high.",
79
+ evidence: `${unnamed.length} of ${conflicts.length} requirement-conflict test(s) named by no severe issue: ${unnamed.map((test) => test.fileName).join(", ")}`,
80
+ suggestion: "Set `plannedTestId` on the `issuesFound` entry that records each conflict, so the issue names the test that asserts it. One severe issue covering an unrelated defect does not stand in for a conflict nobody wrote up.",
81
+ },
82
+ ];
83
+ return [];
84
+ }
@@ -0,0 +1,10 @@
1
+ import { Verifier } from "../types.js";
2
+ /** Verifier 8. A UI planned test opens `elements.pageUrl`; the file it is about
3
+ * renders on some set of routes. Those two have to be the same page, or the
4
+ * trace records a screen the diff did not change.
5
+ *
6
+ * Mechanical, and needs no browser: the route patterns come from the repository
7
+ * scan the caller already runs, and the comparison is segment-wise. SILENT
8
+ * wherever it cannot state a fact — an unknown framework, an unstated file, a
9
+ * pageUrl another check already objects to. Plan time only. */
10
+ export declare const screenRoute: Verifier;
@@ -0,0 +1,118 @@
1
+ import { isUIPlannedTest } from "../subjectStep.js";
2
+ import { SCREEN_ROUTE_CONTRACT } from "../verifierContracts.js";
3
+ /** A route PATTERN's parameter segment, in the four spellings the frameworks this
4
+ * run meets use: TanStack `$id`, Express/Vue `:id`, Next `[id]`, OpenAPI-ish
5
+ * `{id}`. A pattern segment matches any one concrete segment; it never spans
6
+ * two, so depth still has to agree. */
7
+ function isParameter(segment) {
8
+ return (segment.startsWith("$") ||
9
+ segment.startsWith(":") ||
10
+ (segment.startsWith("[") && segment.endsWith("]")) ||
11
+ (segment.startsWith("{") && segment.endsWith("}")));
12
+ }
13
+ function segments(path) {
14
+ return path.split("/").filter(Boolean);
15
+ }
16
+ /** Whether a concrete url path is that route pattern with its parameters filled
17
+ * in. Depth agrees or nothing does: a prefix is a different page, and the run
18
+ * this check exists for opened the PARENT of the route it needed. */
19
+ function fills(pattern, path) {
20
+ const wanted = segments(pattern);
21
+ const actual = segments(path);
22
+ if (wanted.length !== actual.length)
23
+ return false;
24
+ return wanted.every((segment, i) => isParameter(segment) || segment === actual[i]);
25
+ }
26
+ /** The path half of a url, trailing slash dropped, root kept as `/`. A pageUrl is
27
+ * a route path on a lane where no host serves the app and an absolute url when
28
+ * one does; both name the same page. */
29
+ function urlPath(raw) {
30
+ const value = String(raw ?? "").trim();
31
+ if (!value)
32
+ return "";
33
+ try {
34
+ return new URL(value).pathname.replace(/\/+$/, "") || "/";
35
+ }
36
+ catch {
37
+ return value.startsWith("/") ? value.split(/[?#]/)[0].replace(/\/+$/, "") || "/" : "";
38
+ }
39
+ }
40
+ function field(value) {
41
+ return typeof value === "string" ? value.trim() : "";
42
+ }
43
+ /** The routes the resolver gives for a file, or undefined when it cannot speak
44
+ * for it. A resolver that throws or answers with the wrong type is treated as
45
+ * one that cannot speak: a scan is best-effort, and its failure is not evidence
46
+ * about the plan. */
47
+ function routesFor(ctx, file) {
48
+ if (typeof ctx?.routesRenderingFile !== "function")
49
+ return undefined;
50
+ try {
51
+ const routes = ctx.routesRenderingFile(file);
52
+ return Array.isArray(routes) ? routes.map(field).filter(Boolean) : undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ function list(values) {
59
+ return values.join("; ");
60
+ }
61
+ /** Verifier 8. A UI planned test opens `elements.pageUrl`; the file it is about
62
+ * renders on some set of routes. Those two have to be the same page, or the
63
+ * trace records a screen the diff did not change.
64
+ *
65
+ * Mechanical, and needs no browser: the route patterns come from the repository
66
+ * scan the caller already runs, and the comparison is segment-wise. SILENT
67
+ * wherever it cannot state a fact — an unknown framework, an unstated file, a
68
+ * pageUrl another check already objects to. Plan time only. */
69
+ export const screenRoute = {
70
+ name: "screenRoute",
71
+ run(registration, ctx) {
72
+ const objections = [];
73
+ for (const plannedTest of registration.plannedTests ?? []) {
74
+ if (!isUIPlannedTest(plannedTest))
75
+ continue;
76
+ const elements = plannedTest.declarations?.elements;
77
+ const url = field(elements && typeof elements === "object" ? elements.pageUrl : "");
78
+ // `uiElementGrounded:pagecontext` already objects to an empty pageUrl.
79
+ if (!url)
80
+ continue;
81
+ const path = urlPath(url);
82
+ if (!path)
83
+ continue;
84
+ // `screenEvidence.file` names the file that renders the screen; on a mixed
85
+ // PR that is not the file the test is about. `changedFile` is the fallback,
86
+ // and `changedFile:notStated` already objects when neither is stated.
87
+ const screen = field(plannedTest.declarations?.screenEvidence?.file) || field(plannedTest.declarations?.changedFile);
88
+ if (!screen)
89
+ continue;
90
+ const routes = routesFor(ctx, screen);
91
+ // The resolver does not know this file. Not a fact about the plan.
92
+ if (routes === undefined)
93
+ continue;
94
+ if (routes.length === 0) {
95
+ objections.push({
96
+ objectionId: `screenRoute:unmounted:${plannedTest.plannedTestId}`,
97
+ verifier: "screenRoute",
98
+ plannedTestId: plannedTest.plannedTestId,
99
+ message: SCREEN_ROUTE_CONTRACT.objections.unmounted.message,
100
+ evidence: `${screen} is rendered by no route this run found; this test opens ${url}`,
101
+ suggestion: SCREEN_ROUTE_CONTRACT.objections.unmounted.suggestion,
102
+ });
103
+ continue;
104
+ }
105
+ if (routes.some((route) => fills(route, path)))
106
+ continue;
107
+ objections.push({
108
+ objectionId: `screenRoute:mismatch:${plannedTest.plannedTestId}`,
109
+ verifier: "screenRoute",
110
+ plannedTestId: plannedTest.plannedTestId,
111
+ message: SCREEN_ROUTE_CONTRACT.objections.mismatch.message,
112
+ evidence: `${screen} is rendered on: ${list(routes)}; this test opens ${url}`,
113
+ suggestion: SCREEN_ROUTE_CONTRACT.objections.mismatch.suggestion,
114
+ });
115
+ }
116
+ return objections;
117
+ },
118
+ };
@@ -0,0 +1,6 @@
1
+ import { Verifier } from "../types.js";
2
+ /** Verifier 3. Two planned tests whose subject steps name the same method and path
3
+ * must say how they differ — ASKING, where the old dedup key removed one of the
4
+ * pair on a string that says nothing about what either asserts. Both sides get an
5
+ * objection; a stated difference is taken at its word. No request, no compare. */
6
+ export declare const statedDifference: Verifier;