@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** What each plan-time check SAYS to the agent: the objection it emits, and the
|
|
2
|
+
* one-line question that closes it. Each suggestion also carries the judgment
|
|
3
|
+
* the check itself cannot make — the agent reads it at the moment it matters,
|
|
4
|
+
* when the objection arrives, and the skill does not repeat it. What to put in
|
|
5
|
+
* a field, and what the check reads, is stated once in that field's own
|
|
6
|
+
* `describe` in the plan tool's schema. A check compares a change the plan states
|
|
7
|
+
* against something the server already holds, needs no new parser or threshold,
|
|
8
|
+
* and can be answered by the agent. */
|
|
9
|
+
// Evidence is deliberately absent from every contract. Each message below is
|
|
10
|
+
// emitted beside an evidence line built from the planned test's own values, so the
|
|
11
|
+
// evidence stays computed in the verifier.
|
|
12
|
+
export const CHANGED_FILE_CONTRACT = {
|
|
13
|
+
id: "changedFile",
|
|
14
|
+
declarationFields: ["declarations.changedFile", "declarations.screenEvidence.file"],
|
|
15
|
+
objections: {
|
|
16
|
+
notStated: {
|
|
17
|
+
message: "This test does not state which changed file it targets.",
|
|
18
|
+
suggestion: "Name the repository-relative file this test is about in `changedFile`, spelled as the diff spells it.",
|
|
19
|
+
},
|
|
20
|
+
unrelated: {
|
|
21
|
+
message: "The file this test names as its changed file is not one this pull request changed.",
|
|
22
|
+
suggestion: "Name a file from this PR's own diff in `changedFile`, repository-relative and spelled as the diff spells it — or say why this test belongs in the plan without one.",
|
|
23
|
+
},
|
|
24
|
+
unknownRepository: {
|
|
25
|
+
message: "This planned test names a repository this run did not analyze.",
|
|
26
|
+
suggestion: "Name one of the repositories listed in the evidence, or leave `repository` out on a single-repository run. A name nothing analyzed leaves the planned test with no diff to be checked against.",
|
|
27
|
+
},
|
|
28
|
+
screenEvidenceUnrelated: {
|
|
29
|
+
message: "The file this UI test cites for its screen is not one this pull request changed.",
|
|
30
|
+
suggestion: "Which changed file renders the screen this test opens — or which changed screen should it open instead? Name that file in `screenEvidence.file`, repository-relative, spelled as the diff spells it.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
suggestion: "Which file that this pull request changed does this test target?",
|
|
34
|
+
};
|
|
35
|
+
export const SCREEN_ROUTE_CONTRACT = {
|
|
36
|
+
id: "screenRoute",
|
|
37
|
+
declarationFields: ["declarations.elements.pageUrl", "declarations.screenEvidence.file", "declarations.changedFile"],
|
|
38
|
+
objections: {
|
|
39
|
+
mismatch: {
|
|
40
|
+
message: "The page this UI test opens is not a page that renders the changed file it is about.",
|
|
41
|
+
suggestion: "Open one of the routes in the evidence, or name the changed file that does render this page in `screenEvidence.file`. `skyramp_resolve_screen` gives the url to open, with the route parameters it can fill. A trace recorded on a screen the diff did not change proves nothing about the change.",
|
|
42
|
+
},
|
|
43
|
+
unmounted: {
|
|
44
|
+
message: "No route this run found renders the file this UI test is about.",
|
|
45
|
+
suggestion: "If a route in this PR mounts it, name that route in `elements.pageUrl`. If nothing mounts it, the component is unintegrated: say so in one line and keep the planned test out of the browser.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
suggestion: "Does the page this test opens render the changed file the test is about?",
|
|
49
|
+
};
|
|
50
|
+
export const ENDPOINT_GROUNDED_CONTRACT = {
|
|
51
|
+
id: "endpointGrounded",
|
|
52
|
+
declarationFields: ["steps[]", "testType", "declarations.routes"],
|
|
53
|
+
objections: {
|
|
54
|
+
uncited: {
|
|
55
|
+
message: "A call this test makes cites no file that declares it.",
|
|
56
|
+
suggestion: "Add a `routes` entry naming the repository file that declares it — the route or operation need not appear verbatim in that file. A route the endpoint catalog missed is evidence of nothing: cite the file and keep the planned test.",
|
|
57
|
+
},
|
|
58
|
+
disqualifiedCitation: {
|
|
59
|
+
message: "The file this test cites for a call cannot be the file that declares it.",
|
|
60
|
+
suggestion: "Cite the source file that declares it — not a test, not built output, not documentation. Any language or format is accepted, and the route or operation need not appear verbatim in the file.",
|
|
61
|
+
},
|
|
62
|
+
uncitedAndDisqualified: {
|
|
63
|
+
message: "This test has a call with no citation, and a call whose cited file cannot be the file that declares it.",
|
|
64
|
+
suggestion: "Add `routes` for the call that has none, and point the other citation at the source file that declares its route or operation. Neither need appear verbatim in the file.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
suggestion: "Which repository file declares this route or operation, and does an entry name that step's `order`?",
|
|
68
|
+
};
|
|
69
|
+
export const STATED_DIFFERENCE_CONTRACT = {
|
|
70
|
+
id: "statedDifference",
|
|
71
|
+
declarationFields: ["steps[]", "testType", "scenarioName", "declarations.differsFrom"],
|
|
72
|
+
objections: {
|
|
73
|
+
unexplainedPair: {
|
|
74
|
+
message: "Another planned test in this plan tests the same endpoint and neither says how they differ.",
|
|
75
|
+
suggestion: "Two tests on one endpoint is a legitimate plan. This asks HOW they differ, not whether one should go. Add a `differsFrom` entry naming the other planned test and what this one asserts that it does not — or, if one really is redundant, drop it.",
|
|
76
|
+
},
|
|
77
|
+
unknownPartner: {
|
|
78
|
+
message: "A `differsFrom` entry names a partner that is not a planned test in this plan.",
|
|
79
|
+
suggestion: "Spell the partner's `scenarioName` exactly as it appears in your own planned test list — the match is exact apart from surrounding space, so a difference in case names nothing.",
|
|
80
|
+
},
|
|
81
|
+
subjectNotStated: {
|
|
82
|
+
message: "This test makes more than one call and does not say which one it is about.",
|
|
83
|
+
suggestion: "Set `declarations.stepUnderTest` to the `order` of the step under test. The other calls are setup, and nothing on the server ranks them for you.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
suggestion: "Which call is this test about, and what does it assert that the other planned test on the same endpoint does not?",
|
|
87
|
+
};
|
|
88
|
+
export const EXISTING_COVERAGE_CONTRACT = {
|
|
89
|
+
id: "existingCoverage",
|
|
90
|
+
declarationFields: ["declarations.existingTests"],
|
|
91
|
+
objections: {
|
|
92
|
+
citedTestMissing: {
|
|
93
|
+
message: "This planned test names an existing test file that is not in the checkout.",
|
|
94
|
+
suggestion: "Name the file repository-relative, spelled as the analysis lists it. Drop the entry if it names a test you did not read — naming none is an answer too.",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
suggestion: "Which existing tests did you read on this change, and how does this planned test differ from them?",
|
|
98
|
+
};
|
|
99
|
+
export const EXPECTED_OUTCOME_CONTRACT = {
|
|
100
|
+
id: "expectedOutcome",
|
|
101
|
+
declarationFields: ["category", "declarations.expected.outcome", "declarations.expected.why"],
|
|
102
|
+
objections: {
|
|
103
|
+
// `{category}` is the planned test's own `category` value, quoted back to it so
|
|
104
|
+
// the agent recognises the value it sent.
|
|
105
|
+
passInShouldFailCategory: {
|
|
106
|
+
message: 'This planned test is categorised "{category}" but expects to pass on the app as it stands.',
|
|
107
|
+
suggestion: "If it catches a defect, it should be red until the defect is fixed — say so. If it pins behaviour that is already correct, change the category.",
|
|
108
|
+
},
|
|
109
|
+
// Two branches because the old single one told an agent to fix its endpoint
|
|
110
|
+
// path when the unmounted route WAS the critical defect it had just reported
|
|
111
|
+
// — four objections for being honest about `reachedBehavior` (run
|
|
112
|
+
// 33988304641, P11), which the run that wrote `true` never drew.
|
|
113
|
+
executed: {
|
|
114
|
+
message: "This test failed, but it did not reach the behavior it was written for — it fell over earlier. A failure like this is not a caught bug.",
|
|
115
|
+
suggestion: "If the blocker is the test's own setup — sign-in, token, a wrong path — fix it and run the test again. If the blocker is the defect this run reports (a route that is not mounted, a server error before the behaviour), answer this objection naming that issue: the failure then stands as evidence of that issue, not as a caught bug.",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
suggestion: "Should this test fail until the defect is fixed, or is the category wrong for a test that pins behaviour the app already has?",
|
|
119
|
+
};
|
|
120
|
+
export const UI_ELEMENT_GROUNDED_CONTRACT = {
|
|
121
|
+
id: "uiElementGrounded",
|
|
122
|
+
declarationFields: [
|
|
123
|
+
"testType",
|
|
124
|
+
"steps[]",
|
|
125
|
+
"declarations.elements.items",
|
|
126
|
+
"declarations.elements.pageUrl",
|
|
127
|
+
"declarations.asserts",
|
|
128
|
+
"scenarioName",
|
|
129
|
+
"description",
|
|
130
|
+
"declarations.expected.why",
|
|
131
|
+
],
|
|
132
|
+
objections: {
|
|
133
|
+
nogrounding: {
|
|
134
|
+
message: "This UI test names no `elements.items`, so nothing says the elements it targets are on the page.",
|
|
135
|
+
suggestion: "Copy the elements this test targets out of the capture of the page it runs on into `elements.items`, and set `elements.pageUrl` to that page. If no captured page holds this test's elements — the changed component mounts nowhere the run reached, or the screen is behind a gate this run could not pass — answer this objection saying which, and leave `elements.items` null.",
|
|
136
|
+
},
|
|
137
|
+
duplicate: {
|
|
138
|
+
message: "`elements.items` names the same element more than once.",
|
|
139
|
+
suggestion: "List each element once. Two entries that agree on role, name and identifiers describe one element, and the repeat states nothing the first entry did not.",
|
|
140
|
+
},
|
|
141
|
+
pagecontext: {
|
|
142
|
+
message: "This UI test names `elements.items` but no page they came from.",
|
|
143
|
+
suggestion: "Set `elements.pageUrl` to the `url` of the capture the elements were lifted from. Without it there is no page to look them up on.",
|
|
144
|
+
},
|
|
145
|
+
ungrounded: {
|
|
146
|
+
message: "An element this UI test targets is in no blueprint captured for that page.",
|
|
147
|
+
suggestion: "Cite what a capture shows you, never what the screen ought to have. A capture shows the page as it stood when it was taken, so an element behind a modal, a dropdown or an accordion is legitimately absent — answer this objection naming the action that reveals it. Otherwise copy the element from the capture as it is spelled there, or point `elements.pageUrl` at the page you actually captured.",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
// Reconciled after the fix round, which is what the TO-RECONCILE marker here
|
|
151
|
+
// was waiting on. It phrases the question and names no phrase that closes the
|
|
152
|
+
// check — Ruling B removed exactly such a phrase from verifier 8's suggestion.
|
|
153
|
+
suggestion: "Which capture of the page does each declared element come from, and for one that is in no capture, which action on that page reveals it?",
|
|
154
|
+
};
|
|
155
|
+
export const COVERAGE_CONTRACT = {
|
|
156
|
+
id: "coverage",
|
|
157
|
+
declarationFields: [
|
|
158
|
+
"changes",
|
|
159
|
+
"declarations.changes",
|
|
160
|
+
"declarations.changedFile",
|
|
161
|
+
"declarations.routes",
|
|
162
|
+
"declarations.screenEvidence.file",
|
|
163
|
+
"declarations.startState",
|
|
164
|
+
"changes[].cases",
|
|
165
|
+
"changes[].surfaces",
|
|
166
|
+
],
|
|
167
|
+
objections: {
|
|
168
|
+
change: {
|
|
169
|
+
message: "This declared change has no test in the plan.",
|
|
170
|
+
suggestion: "Plan a test that exercises it. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, a paired branch that no longer exists, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.",
|
|
171
|
+
},
|
|
172
|
+
noChanges: {
|
|
173
|
+
message: "The plan declares no changes, so nothing says what the pull request must make different.",
|
|
174
|
+
suggestion: "List the behaviours the pull request description promises, one sentence each, and cite them from the planned tests that test them — or answer in one line that it promises none, which a docs-only or cosmetic change legitimately does.",
|
|
175
|
+
},
|
|
176
|
+
unknownChange: {
|
|
177
|
+
message: "A planned test cites a change id that this plan does not declare.",
|
|
178
|
+
suggestion: "Spell the change's `id` exactly as your own `changes` list gives it — the match is exact apart from surrounding space. Declare the change if it is missing.",
|
|
179
|
+
},
|
|
180
|
+
file: {
|
|
181
|
+
message: "This changed file has no test in the plan.",
|
|
182
|
+
suggestion: "Add a planned test that cites it — as `changedFile`, as a `routes` file, or as `screenEvidence.file` — or answer in ONE line why it needs none: config only, covered by the test on the route it serves, or style-only and covered by the UI test on the page it belongs to. This check is a floor, never a cap: it never asks you to plan fewer tests.",
|
|
183
|
+
},
|
|
184
|
+
// THE ONE OBJECTION NO ANSWER CLOSES, and `registerPlan` is what holds that.
|
|
185
|
+
// Twelve runs over 2026-09-04 and 05 missed the same kind of planted bug: a
|
|
186
|
+
// rule about a STORED value the new code reads and never wrote, which only a
|
|
187
|
+
// record already in another state reaches. Prose reached the agent twice and
|
|
188
|
+
// was skipped twice,
|
|
189
|
+
// so an answer here would close the objection and leave the bug.
|
|
190
|
+
// `{route}` is the mutated route, as `METHOD path`.
|
|
191
|
+
stateTest: {
|
|
192
|
+
message: "No test in this plan names the state the record is in when {route} mutates it.",
|
|
193
|
+
suggestion: "Plan one test that puts a record in a stated state — through the API, the UI, or data the app already holds; a record with no rows yet counts — then runs the mutation on it, and name that state as `startState`. If the route can meet a record that is not freshly created, use that state. Only a planned test closes this: there is no answer for it.",
|
|
194
|
+
},
|
|
195
|
+
// `{missing}` is the declared cases no step sends, as `param=value (expect)`.
|
|
196
|
+
// The check reads the values the citing tests send, so a case the agent
|
|
197
|
+
// declares and never sends is now visible: four runs on one fixture declared
|
|
198
|
+
// a 1-100 range and sent the upper end once, and one report claimed both ends
|
|
199
|
+
// anyway. What a case IS stays the agent's judgment — the field takes any
|
|
200
|
+
// parameter and any value, so nothing here has to know a kind of input in
|
|
201
|
+
// advance.
|
|
202
|
+
cases: {
|
|
203
|
+
message: "No test for this change sends the declared case {missing}.",
|
|
204
|
+
suggestion: "Add the missing values to a test that cites this change, or answer in one line why they need no test.",
|
|
205
|
+
},
|
|
206
|
+
// `{param}` is the parameter and `{value}` the placeholder it accepts. Read off
|
|
207
|
+
// the plan alone: a placeholder value is a record an earlier step created, so a
|
|
208
|
+
// route that takes it by id can also be given one that is not there. A DELETE on
|
|
209
|
+
// a collection member answered 200 for a user that was never a member, and no
|
|
210
|
+
// plan of six runs listed that rejection.
|
|
211
|
+
reject: {
|
|
212
|
+
message: "`{param}` accepts `{value}`, a record the plan creates, and no case rejects a record that is not there or belongs to another parent.",
|
|
213
|
+
suggestion: "Add a reject case on `{param}` with a value that is not that record and a step that sends it, or answer why the route cannot refuse it.",
|
|
214
|
+
},
|
|
215
|
+
// `{kind}` is the test types that reach the surface and `{surface}` is the
|
|
216
|
+
// surface itself. Run 34065056830 cited a page change — a colour filter the
|
|
217
|
+
// pull request added to a page — from an API test, and no test in the plan
|
|
218
|
+
// ever clicked the filter.
|
|
219
|
+
surface: {
|
|
220
|
+
message: "No {kind} test cites this change, which is on the {surface}.",
|
|
221
|
+
suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?",
|
|
225
|
+
};
|
|
226
|
+
export const REMOVED_ELEMENT_GUARDED_CONTRACT = {
|
|
227
|
+
id: "removedElementGuarded",
|
|
228
|
+
declarationFields: ["declarations.elements.items", "declarations.asserts"],
|
|
229
|
+
objections: {
|
|
230
|
+
// v1 resolved a removed element to a page in the server, and fell back to the
|
|
231
|
+
// root pages when the import graph found none, so that an element whose page
|
|
232
|
+
// could not be resolved still got a guard (SKYR-4339). v2 does not attribute
|
|
233
|
+
// pages, so the same protection reads the plan: the run picks the page, and
|
|
234
|
+
// has to pick one.
|
|
235
|
+
missing: {
|
|
236
|
+
message: "The diff removes `{element}` and no planned test says it is gone.",
|
|
237
|
+
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.",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
suggestion: "Does every element this diff removes have a planned test that asserts it is gone?",
|
|
241
|
+
};
|
|
242
|
+
/** In the order of `PLAN_TIME_VERIFIERS`, which decides the order objections are
|
|
243
|
+
* reported in and carries no priority. References, not copies: iterating this
|
|
244
|
+
* array, or indexing it with a variable, widens the element to the union of them
|
|
245
|
+
* all and then no `objections` key resolves. */
|
|
246
|
+
export const VERIFIER_CONTRACTS = [
|
|
247
|
+
CHANGED_FILE_CONTRACT,
|
|
248
|
+
ENDPOINT_GROUNDED_CONTRACT,
|
|
249
|
+
STATED_DIFFERENCE_CONTRACT,
|
|
250
|
+
EXISTING_COVERAGE_CONTRACT,
|
|
251
|
+
EXPECTED_OUTCOME_CONTRACT,
|
|
252
|
+
UI_ELEMENT_GROUNDED_CONTRACT,
|
|
253
|
+
SCREEN_ROUTE_CONTRACT,
|
|
254
|
+
COVERAGE_CONTRACT,
|
|
255
|
+
REMOVED_ELEMENT_GUARDED_CONTRACT,
|
|
256
|
+
];
|
|
257
|
+
/** Fills a contract sentence's `{placeholder}` spans from the run's or the
|
|
258
|
+
* planned test's own values. A placeholder with no value is left as written rather
|
|
259
|
+
* than blanked, so a missed key shows up as `{knob}` instead of reading as a gap
|
|
260
|
+
* in an otherwise ordinary sentence. */
|
|
261
|
+
export function fillPlaceholders(text, values) {
|
|
262
|
+
return text.replace(/\{(\w+)\}/g, (whole, key) => (key in values ? String(values[key]) : whole));
|
|
263
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { allChangedFiles, changedFilesFor } from "../types.js";
|
|
2
|
+
import { CHANGED_FILE_CONTRACT } from "../verifierContracts.js";
|
|
3
|
+
import { changedPathSet, normalizeCitedPath, whyNotAChangedFile } from "./citedPath.js";
|
|
4
|
+
/** Verifier 1. The files a planned test cites for the change it targets are files
|
|
5
|
+
* this pull request really changed. NAMES ONLY — no diff content is read, so an
|
|
6
|
+
* irrelevant file inside the diff cannot be caught. SILENT on a run with no
|
|
7
|
+
* diff; where one repository has a diff and another does not, the reason says so.
|
|
8
|
+
*
|
|
9
|
+
* The planned test used to declare the changed LINE here as well. It was recorded
|
|
10
|
+
* and never compared against anything, and the reasoning a reviewer wants from
|
|
11
|
+
* it belongs in the planned test's own description. */
|
|
12
|
+
/** How a malformed declaration reads back to the agent. */
|
|
13
|
+
function describeType(value) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return "missing";
|
|
16
|
+
if (value === null)
|
|
17
|
+
return "null";
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return "an array";
|
|
20
|
+
return `a ${typeof value}`;
|
|
21
|
+
}
|
|
22
|
+
/** One objection, so every branch below reads the same way. */
|
|
23
|
+
function raise(plannedTest, key, objection, evidence) {
|
|
24
|
+
return {
|
|
25
|
+
objectionId: `changedFile:${key}:${plannedTest?.plannedTestId}`,
|
|
26
|
+
verifier: "changedFile",
|
|
27
|
+
plannedTestId: plannedTest?.plannedTestId,
|
|
28
|
+
message: objection.message,
|
|
29
|
+
evidence,
|
|
30
|
+
suggestion: objection.suggestion,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export const changedFile = {
|
|
34
|
+
name: "changedFile",
|
|
35
|
+
run(registration, ctx) {
|
|
36
|
+
const objections = [];
|
|
37
|
+
// No diff anywhere in the run: no changed file to cite, so nothing to say.
|
|
38
|
+
if (allChangedFiles(ctx).length === 0)
|
|
39
|
+
return objections;
|
|
40
|
+
for (const plannedTest of registration.plannedTests) {
|
|
41
|
+
// Per planned test, from the planned test's OWN repository where it names one.
|
|
42
|
+
// Reading every repository's instead let a planned test for one repository
|
|
43
|
+
// cite a file only the other one changed and pass a check named for its
|
|
44
|
+
// repository.
|
|
45
|
+
const own = changedFilesFor(ctx, plannedTest);
|
|
46
|
+
if (own === undefined) {
|
|
47
|
+
objections.push(raise(plannedTest, "unknownRepository", CHANGED_FILE_CONTRACT.objections.unknownRepository, `this run analyzed ${Object.keys(ctx.changedFiles ?? {}).join(", ") || "no repository"}, and this planned test names "${String(plannedTest?.repository ?? "").trim()}"`));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const surfaces = changedPathSet(own, true);
|
|
51
|
+
// A deleted file is a changed surface but not a page: a screen that no
|
|
52
|
+
// longer exists cannot be opened, so the screen citation is compared
|
|
53
|
+
// against the files that survive the change.
|
|
54
|
+
const pages = changedPathSet(own, false);
|
|
55
|
+
// `screenEvidence.file` has to be a file the diff changed. Independent of
|
|
56
|
+
// the changed file's own result — a planned test can get both wrong, and each
|
|
57
|
+
// names its own fix.
|
|
58
|
+
const citedScreen = String(plannedTest?.declarations?.screenEvidence?.file ?? "").trim();
|
|
59
|
+
if (citedScreen && !pages.has(normalizeCitedPath(citedScreen))) {
|
|
60
|
+
const page = String(plannedTest?.declarations?.elements?.pageUrl ?? "").trim();
|
|
61
|
+
objections.push(raise(plannedTest, "screenEvidenceUnrelated", CHANGED_FILE_CONTRACT.objections.screenEvidenceUnrelated, `${pages.size} changed file(s) still in the tree. ${whyNotAChangedFile(citedScreen)}, for ${page ? `the test that opens ${page}` : "a test that names no page"}.`));
|
|
62
|
+
}
|
|
63
|
+
// The declared type holds only on the schema-validated path. A crash here
|
|
64
|
+
// ends the run; an objection lets the agent fix the field.
|
|
65
|
+
const raw = plannedTest?.declarations?.changedFile;
|
|
66
|
+
const cited = typeof raw === "string" ? raw.trim() : "";
|
|
67
|
+
if (!cited) {
|
|
68
|
+
objections.push(raise(plannedTest, "notStated", CHANGED_FILE_CONTRACT.objections.notStated, `changedFile is ${describeType(raw)}; ${surfaces.size} file(s) changed in this run and the plan does not say which one this test targets.`));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!surfaces.has(normalizeCitedPath(cited))) {
|
|
72
|
+
// A repository of the run has no diff, so "not among the changed files"
|
|
73
|
+
// is not a fact this check can establish for a planned test from it.
|
|
74
|
+
const unverifiable = ctx.reposWithoutDiff.length > 0
|
|
75
|
+
? ` No branch diff exists for ${ctx.reposWithoutDiff.join(", ")}, so a file from those changes could not be checked.`
|
|
76
|
+
: "";
|
|
77
|
+
objections.push(raise(plannedTest, "unrelated", CHANGED_FILE_CONTRACT.objections.unrelated, `${surfaces.size} changed file(s). ${whyNotAChangedFile(cited)}.${unverifiable}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return objections;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChangedFileName } from "../../utils/branchDiff.js";
|
|
2
|
+
/** A cited path reduced to the spelling `git diff --name-status` produces: `./`,
|
|
3
|
+
* backslashes, doubled and trailing slashes fold, and so do git's quoting of a
|
|
4
|
+
* non-ASCII path and both unicode compositions. Case does NOT fold, and a path
|
|
5
|
+
* leaving the repository normalises to empty, by `escapesRepo`. */
|
|
6
|
+
export declare function normalizeCitedPath(raw: unknown): string;
|
|
7
|
+
/** The changed paths a citation can name, in the same normalised spelling.
|
|
8
|
+
* `includeDeleted` is the caller's decision: a deleted file is still a changed
|
|
9
|
+
* surface a test can cover, and it is NOT a page a UI test can open. */
|
|
10
|
+
export declare function changedPathSet(files: ChangedFileName[], includeDeleted: boolean): Set<string>;
|
|
11
|
+
/** Why a citation is not one of the changed files. */
|
|
12
|
+
export declare function whyNotAChangedFile(file: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { unquoteIfQuoted } from "../../utils/branchDiff.js";
|
|
2
|
+
import { escapesRepo } from "./endpointGrounded.js";
|
|
3
|
+
/** A cited path reduced to the spelling `git diff --name-status` produces: `./`,
|
|
4
|
+
* backslashes, doubled and trailing slashes fold, and so do git's quoting of a
|
|
5
|
+
* non-ASCII path and both unicode compositions. Case does NOT fold, and a path
|
|
6
|
+
* leaving the repository normalises to empty, by `escapesRepo`. */
|
|
7
|
+
export function normalizeCitedPath(raw) {
|
|
8
|
+
if (typeof raw !== "string")
|
|
9
|
+
return "";
|
|
10
|
+
const slashed = unquoteIfQuoted(raw.trim()).normalize("NFC").replace(/\\/g, "/");
|
|
11
|
+
if (!slashed || escapesRepo(slashed))
|
|
12
|
+
return "";
|
|
13
|
+
const segments = slashed.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
14
|
+
return segments.join("/");
|
|
15
|
+
}
|
|
16
|
+
/** The changed paths a citation can name, in the same normalised spelling.
|
|
17
|
+
* `includeDeleted` is the caller's decision: a deleted file is still a changed
|
|
18
|
+
* surface a test can cover, and it is NOT a page a UI test can open. */
|
|
19
|
+
export function changedPathSet(files, includeDeleted) {
|
|
20
|
+
const paths = new Set();
|
|
21
|
+
for (const file of Array.isArray(files) ? files : []) {
|
|
22
|
+
if (!includeDeleted && file?.deleted)
|
|
23
|
+
continue;
|
|
24
|
+
const path = normalizeCitedPath(file?.path);
|
|
25
|
+
if (path)
|
|
26
|
+
paths.add(path);
|
|
27
|
+
}
|
|
28
|
+
return paths;
|
|
29
|
+
}
|
|
30
|
+
/** Why a citation is not one of the changed files. */
|
|
31
|
+
export function whyNotAChangedFile(file) {
|
|
32
|
+
return escapesRepo(file)
|
|
33
|
+
? `cited path "${file}" points outside the repository`
|
|
34
|
+
: `cited file ${file} is not among the files this pull request changed`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Verifier } from "../types.js";
|
|
2
|
+
/** The completeness check, over three surfaces: every declared behaviour, every
|
|
3
|
+
* mutated route, and every changed file needs a test in the plan. More is
|
|
4
|
+
* welcome, fewer is the objection. ONE RULE for the file half, no classification
|
|
5
|
+
* — every changed file is a surface unless it cannot be product code, including a
|
|
6
|
+
* removed one. A FLOOR, never a cap. Only the file half needs a diff. */
|
|
7
|
+
export declare const coverage: Verifier;
|