@skyramp/mcp 0.4.1 → 0.4.2-rc.2
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/localDevTestChangesCommand.js +2 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +15 -7
- package/build/commands/testThisEndpointCommand.js +35 -6
- package/build/execution/wrapperConfig.d.ts +56 -0
- package/build/execution/wrapperConfig.js +155 -0
- package/build/index.js +40 -10
- package/build/playwright/blueprintDigest.js +28 -7
- package/build/playwright/registerPlaywrightTools.js +47 -28
- package/build/playwright/traceExportStore.d.ts +22 -0
- package/build/playwright/traceExportStore.js +81 -0
- package/build/playwright/traceRecordingPrompt.js +11 -3
- package/build/prompts/code-reuse.js +118 -49
- package/build/prompts/fix-error-prompt.d.ts +9 -1
- package/build/prompts/fix-error-prompt.js +31 -28
- package/build/prompts/local-dev/local-dev-plan.d.ts +3 -0
- package/build/prompts/local-dev/local-dev-plan.js +8 -23
- package/build/prompts/local-dev/local-dev-prompts.d.ts +1 -1
- package/build/prompts/local-dev/local-dev-prompts.js +31 -4
- package/build/prompts/modularization/integration-test-modularization.js +13 -6
- package/build/prompts/modularization/ui-test-modularization.js +1 -1
- package/build/prompts/personas.js +1 -1
- package/build/prompts/pom-aware-code-reuse.js +7 -9
- package/build/prompts/reuse-hand-off.d.ts +64 -0
- package/build/prompts/reuse-hand-off.js +130 -0
- package/build/prompts/shared-helper-policy.d.ts +124 -2
- package/build/prompts/shared-helper-policy.js +178 -12
- package/build/prompts/startTraceCollectionPrompts.js +1 -1
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +7 -8
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +1 -1
- package/build/prompts/sut-setup/shared.d.ts +4 -1
- package/build/prompts/sut-setup/shared.js +6 -4
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +19 -11
- package/build/prompts/test-maintenance/actionsInstructions.js +47 -26
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +16 -8
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +93 -38
- package/build/prompts/test-maintenance/driftAnalysisSections.js +5 -3
- package/build/prompts/test-maintenance/driftAnalysisShared.js +4 -2
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -5
- package/build/prompts/test-recommendation/recommendationSections.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationShared.js +0 -1
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +5 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +10 -7
- package/build/prompts/testbot/testbot-prompts.js +133 -63
- package/build/recommendation/answers.js +18 -9
- package/build/recommendation/pullRequestText.js +1 -1
- package/build/recommendation/registerPlan.js +31 -17
- package/build/recommendation/subjectStep.d.ts +8 -10
- package/build/recommendation/subjectStep.js +17 -16
- package/build/recommendation/types.d.ts +71 -17
- package/build/recommendation/types.js +9 -15
- package/build/recommendation/verifierContracts.d.ts +7 -3
- package/build/recommendation/verifierContracts.js +8 -4
- package/build/recommendation/verifiers/changedFile.js +11 -13
- package/build/recommendation/verifiers/citedPath.d.ts +8 -0
- package/build/recommendation/verifiers/citedPath.js +16 -2
- package/build/recommendation/verifiers/coverage.d.ts +2 -2
- package/build/recommendation/verifiers/coverage.js +190 -134
- package/build/recommendation/verifiers/defects.js +56 -16
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +7 -1
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +28 -14
- package/build/recommendation/verifiers/endpointGrounded.js +14 -8
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +30 -21
- package/build/recommendation/verifiers/expectedOutcome.js +17 -8
- package/build/recommendation/verifiers/expectedValueSourced.js +54 -43
- package/build/recommendation/verifiers/issueTraceability.d.ts +5 -0
- package/build/recommendation/verifiers/issueTraceability.js +64 -7
- package/build/recommendation/verifiers/removedElementGuarded.js +19 -11
- package/build/recommendation/verifiers/reportedCategory.js +9 -4
- package/build/recommendation/verifiers/requirementSourced.js +34 -21
- package/build/recommendation/verifiers/screenRoute.js +15 -10
- package/build/recommendation/verifiers/statedDifference.js +15 -8
- package/build/recommendation/verifiers/uiElementGrounded.js +50 -16
- package/build/resources/analysisResources.js +7 -3
- package/build/resources/progressResource.js +4 -2
- package/build/resources/sutSetupResource.js +20 -2
- package/build/resources/testbotResource.js +19 -1
- package/build/services/AnalyticsService.js +3 -1
- package/build/services/ScenarioGenerationService.js +5 -5
- package/build/services/TestDiscoveryService.js +53 -9
- package/build/services/TestExecutionService.js +63 -21
- package/build/services/TestGenerationService.d.ts +1 -1
- package/build/services/TestGenerationService.js +43 -24
- package/build/skills/enhanceAssertionsSkill.d.ts +45 -0
- package/build/skills/enhanceAssertionsSkill.js +103 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +2 -2
- package/build/skills/fixTestImportErrorsSkill.js +2 -2
- package/build/skills/runTestSkill.d.ts +6 -0
- package/build/skills/runTestSkill.js +17 -0
- package/build/skills/skillFiles.d.ts +38 -0
- package/build/skills/skillFiles.js +94 -0
- package/build/skills/validateAssertionAlignmentSkill.d.ts +34 -0
- package/build/skills/validateAssertionAlignmentSkill.js +59 -0
- package/build/tool-phases.js +4 -1
- package/build/tools/auth/loginTool.js +3 -1
- package/build/tools/auth/logoutTool.js +3 -1
- package/build/tools/budgetExcuse.d.ts +15 -0
- package/build/tools/budgetExcuse.js +113 -0
- package/build/tools/code-refactor/alignAssertionsTool.d.ts +2 -0
- package/build/tools/code-refactor/alignAssertionsTool.js +51 -0
- package/build/tools/code-refactor/assertion-state.d.ts +1 -1
- package/build/tools/code-refactor/assertion-state.js +1 -1
- package/build/tools/code-refactor/assertionOperations.d.ts +44 -0
- package/build/tools/code-refactor/assertionOperations.js +82 -0
- package/build/tools/code-refactor/assertionSkillTools.d.ts +3 -0
- package/build/tools/code-refactor/assertionSkillTools.js +86 -0
- package/build/tools/code-refactor/codeReuseTool.js +27 -3
- package/build/tools/code-refactor/enhanceAssertionsTool.js +19 -83
- package/build/tools/code-refactor/gate-markers.d.ts +51 -0
- package/build/tools/code-refactor/gate-markers.js +95 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -1
- package/build/tools/code-refactor/retrofit-state.js +44 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +36 -13
- package/build/tools/code-refactor/reuse-state.d.ts +90 -16
- package/build/tools/code-refactor/reuse-state.js +191 -61
- package/build/tools/code-refactor/utils-verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/utils-verify-gates.js +269 -30
- package/build/tools/code-refactor/verify-gates.d.ts +15 -1
- package/build/tools/code-refactor/verify-gates.js +36 -4
- package/build/tools/enrichTestWithMocksTool.d.ts +1 -1
- package/build/tools/enrichTestWithMocksTool.js +9 -5
- package/build/tools/executeSkyrampTestTool.d.ts +118 -48
- package/build/tools/executeSkyrampTestTool.js +998 -372
- package/build/tools/execution-video-state.js +1 -1
- package/build/tools/fixErrorTool.js +5 -6
- package/build/tools/generate-tests/batchMockGenerationTool.js +1 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +114 -73
- package/build/tools/generate-tests/generateContractRestTool.js +34 -16
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateE2ERestTool.js +9 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.js +22 -7
- package/build/tools/generate-tests/generateMockRestTool.js +3 -1
- package/build/tools/generate-tests/generateUIRestTool.d.ts +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +11 -2
- package/build/tools/generate-tests/loadTestSchema.js +1 -3
- package/build/tools/generate-tests/planGuard.js +6 -3
- package/build/tools/generate-tests/scenarioFileIdentity.js +4 -1
- package/build/tools/generate-tests/scenarioLint.js +17 -5
- package/build/tools/generate-tests/trace-reuse-guard.js +5 -2
- package/build/tools/generateEnrichedIntegrationTestTool.js +9 -3
- package/build/tools/one-click/oneClickTool.js +3 -1
- package/build/tools/preflightMockCheckTool.js +23 -7
- package/build/tools/submitReportTool.d.ts +51 -12
- package/build/tools/submitReportTool.js +988 -161
- package/build/tools/test-management/actionsTool.js +241 -51
- package/build/tools/test-management/analyzeChangesTool.d.ts +8 -9
- package/build/tools/test-management/analyzeChangesTool.js +127 -68
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +0 -11
- package/build/tools/test-management/analyzeTestHealthTool.js +42 -76
- package/build/tools/test-management/registerTestPlanTool.d.ts +44 -34
- package/build/tools/test-management/registerTestPlanTool.js +255 -111
- package/build/tools/test-management/resolveScreenTool.js +33 -9
- package/build/tools/test-management/testsOwedBeforeRun.d.ts +28 -0
- package/build/tools/test-management/testsOwedBeforeRun.js +53 -0
- package/build/tools/trace/startTraceCollectionTool.js +3 -1
- package/build/tools/trace/stopTraceCollectionTool.js +42 -6
- package/build/tools/verifyTestDependenciesTool.d.ts +3 -0
- package/build/tools/verifyTestDependenciesTool.js +54 -0
- package/build/tools/workspace/initScanWorkspaceTool.js +9 -3
- package/build/tools/workspace/initializeWorkspaceTool.js +3 -1
- package/build/types/AssertionOutcome.d.ts +1 -1
- package/build/types/EnhanceType.d.ts +6 -0
- package/build/types/EnhanceType.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +32 -72
- package/build/types/ReuseOutcome.d.ts +100 -7
- package/build/types/ReuseOutcome.js +16 -0
- package/build/types/StepMethod.js +20 -6
- package/build/types/TestAnalysis.d.ts +10 -2
- package/build/types/TestExecution.d.ts +45 -0
- package/build/types/TestRecommendation.d.ts +1 -1
- package/build/types/TestRecommendation.js +4 -1
- package/build/types/TestTypes.d.ts +16 -0
- package/build/types/TestTypes.js +40 -3
- package/build/types/TestbotPromptOptions.d.ts +9 -1
- package/build/types/TestbotReport.d.ts +45 -9
- package/build/utils/AnalysisStateManager.d.ts +137 -34
- package/build/utils/AnalysisStateManager.js +228 -44
- package/build/utils/assertion-verify/api-shared-lints.js +34 -16
- package/build/utils/assertion-verify/metrics.js +39 -6
- package/build/utils/assertion-verify/ui-lints.js +4 -2
- package/build/utils/branchDiff.js +47 -12
- package/build/utils/canonicalJson.js +3 -1
- package/build/utils/connectionErrors.d.ts +10 -0
- package/build/utils/connectionErrors.js +10 -0
- package/build/utils/dartRouteExtractor.js +36 -7
- package/build/utils/fixAttempts.d.ts +26 -0
- package/build/utils/fixAttempts.js +109 -0
- package/build/utils/frontendSelectors.js +23 -4
- package/build/utils/generatedTestRecord.d.ts +19 -0
- package/build/utils/generatedTestRecord.js +61 -0
- package/build/utils/gitStaging.js +7 -2
- package/build/utils/initAgent.js +26 -6
- package/build/utils/language-helper.js +60 -45
- package/build/utils/pathMatching.js +2 -1
- package/build/utils/pathSignatures.js +6 -2
- package/build/utils/planMatchKeys.d.ts +2 -2
- package/build/utils/planMatchKeys.js +20 -14
- package/build/utils/pom-catalog-parse.js +7 -2
- package/build/utils/pom-scope/import-expansion.js +6 -1
- package/build/utils/pom-scope/index.js +50 -12
- package/build/utils/pom-scope/scoring.js +13 -3
- package/build/utils/pom-scope/selector-extractor.js +16 -3
- package/build/utils/pom-scope/strip.d.ts +8 -0
- package/build/utils/pom-scope/strip.js +238 -0
- package/build/utils/pom-scope/testIdDiscovery.js +14 -2
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +1 -1
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +4 -2
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +3 -1
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +3 -1
- package/build/utils/pom-verify/bindings.js +5 -1
- package/build/utils/pom-verify/calls.js +9 -2
- package/build/utils/pom-verify/verify.js +27 -5
- package/build/utils/pr-comment-parser.js +20 -7
- package/build/utils/progress.d.ts +1 -1
- package/build/utils/progress.js +1 -1
- package/build/utils/proxy-terminal.d.ts +19 -1
- package/build/utils/proxy-terminal.js +346 -21
- package/build/utils/rebaselineSnapshots.d.ts +1 -1
- package/build/utils/rebaselineSnapshots.js +6 -16
- package/build/utils/removedUiElements.js +1 -1
- package/build/utils/reportLanguage.js +35 -7
- package/build/utils/reportVerification.d.ts +14 -8
- package/build/utils/reportVerification.js +19 -19
- package/build/utils/repositorySlug.d.ts +32 -0
- package/build/utils/repositorySlug.js +77 -0
- package/build/utils/reuseRouting.d.ts +10 -0
- package/build/utils/reuseRouting.js +21 -2
- package/build/utils/runContextGauge.d.ts +27 -0
- package/build/utils/runContextGauge.js +181 -0
- package/build/utils/runSerialized.d.ts +3 -0
- package/build/utils/runSerialized.js +39 -0
- package/build/utils/screenRoutes.js +74 -18
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/skyrampSdkVersion.d.ts +9 -0
- package/build/utils/skyrampSdkVersion.js +16 -0
- package/build/utils/sourceRouteExtractor.js +13 -6
- package/build/utils/telemetry.d.ts +1 -0
- package/build/utils/telemetry.js +8 -5
- package/build/utils/testDependencyPolicy.d.ts +9 -0
- package/build/utils/testDependencyPolicy.js +809 -0
- package/build/utils/testExecutionRecord.d.ts +94 -0
- package/build/utils/testExecutionRecord.js +269 -0
- package/build/utils/testFileClassification.d.ts +8 -0
- package/build/utils/testFileClassification.js +39 -4
- package/build/utils/trace-parser.js +62 -14
- package/build/utils/urlPath.js +3 -1
- package/build/utils/utils-verify/action-key.d.ts +46 -0
- package/build/utils/utils-verify/action-key.js +120 -38
- package/build/utils/utils-verify/action-sites.d.ts +32 -0
- package/build/utils/utils-verify/action-sites.js +202 -0
- package/build/utils/utils-verify/allow.d.ts +122 -3
- package/build/utils/utils-verify/allow.js +146 -21
- package/build/utils/utils-verify/body-reach.d.ts +120 -0
- package/build/utils/utils-verify/body-reach.js +333 -0
- package/build/utils/utils-verify/call-sites.d.ts +31 -7
- package/build/utils/utils-verify/call-sites.js +80 -13
- package/build/utils/utils-verify/delivered-imports.d.ts +43 -0
- package/build/utils/utils-verify/delivered-imports.js +84 -0
- package/build/utils/utils-verify/fixed-sleep.d.ts +96 -0
- package/build/utils/utils-verify/fixed-sleep.js +461 -0
- package/build/utils/utils-verify/in-house.d.ts +93 -0
- package/build/utils/utils-verify/in-house.js +719 -0
- package/build/utils/utils-verify/incumbent.d.ts +3 -0
- package/build/utils/utils-verify/incumbent.js +75 -0
- package/build/utils/utils-verify/index.d.ts +2 -0
- package/build/utils/utils-verify/index.js +2 -0
- package/build/utils/utils-verify/language-spec.d.ts +51 -10
- package/build/utils/utils-verify/language-spec.js +232 -21
- package/build/utils/utils-verify/locate.d.ts +26 -0
- package/build/utils/utils-verify/locate.js +125 -23
- package/build/utils/utils-verify/module-name.d.ts +40 -0
- package/build/utils/utils-verify/module-name.js +98 -0
- package/build/utils/utils-verify/parse.d.ts +56 -7
- package/build/utils/utils-verify/parse.js +195 -40
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +5 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +4 -2
- package/build/utils/utils-verify/stage.d.ts +5 -0
- package/build/utils/utils-verify/stage.js +47 -2
- package/build/utils/utils-verify/status-once.d.ts +62 -0
- package/build/utils/utils-verify/status-once.js +207 -0
- package/build/utils/utils-verify/typecheck.d.ts +61 -0
- package/build/utils/utils-verify/typecheck.js +338 -0
- package/build/utils/utils-verify/verify.d.ts +79 -3
- package/build/utils/utils-verify/verify.js +531 -61
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.js +107 -37
- package/build/workspace/queryParamResolution.js +11 -4
- package/build/workspace/workspace.d.ts +72 -52
- package/build/workspace/workspace.js +19 -15
- package/node_modules/playwright/ThirdPartyNotices.txt +19 -19
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +8 -2
- package/node_modules/playwright/lib/mcp/skyramp/common/cssValue.js +87 -0
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +3 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +181 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +24 -0
- package/node_modules/playwright/lib/transform/babelBundleImpl.js +2353 -190
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +62 -34
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.package-lock.json +72 -41
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/@hono/node-server/dist/serve-static.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/@hono/node-server/dist/serve-static.mjs +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/@hono/node-server/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/README.md +16 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/index.js +4 -20
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/read.js +17 -17
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/json.js +60 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/raw.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/text.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/urlencoded.js +16 -20
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/utils.js +18 -16
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/LICENSE +22 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/README.md +71 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +46 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/dist/index.js +176 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/package.json +52 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/package.json +23 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/CHANGELOG.md +21 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/isObject.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/package.json +6 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/tsconfig.json +1 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/index.js +266 -45
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/lib/utils.js +396 -92
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/equal.test.js +31 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/ipv6-validation.test.js +124 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/parse.test.js +7 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/security.test.js +301 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hasown/CHANGELOG.md +7 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hasown/index.d.ts +0 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hasown/package.json +4 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/client.js +30 -16
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/utils.js +4 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/context.js +32 -13
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/proxy/index.js +4 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/ssg/ssg.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/ssg/utils.js +30 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/streaming/sse.js +5 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/hono-base.js +10 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/base.js +44 -23
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/components.js +25 -26
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/context.js +5 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/dom/server.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/hooks/index.js +16 -13
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/streaming.js +4 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cache/index.js +103 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/compress/index.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cors/index.js +17 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/csrf/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/digest.js +47 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/index.js +7 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/jwk/jwk.js +9 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/jwt/jwt.js +9 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/language/language.js +10 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/method-not-allowed/index.js +90 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +16 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/request.js +20 -13
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/linear-router/router.js +7 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/pattern-router/router.js +3 -9
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +65 -59
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +71 -128
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +14 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/node.js +47 -70
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/router.js +3 -11
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/utils.js +27 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/accept.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/body.js +21 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/cookie.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/ipaddr.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/stream.js +12 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/url.js +19 -11
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/client.js +30 -16
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/utils.js +4 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/context.js +32 -13
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/proxy/index.js +4 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/ssg/ssg.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/ssg/utils.js +30 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/streaming/sse.js +5 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/hono-base.js +10 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/base.js +41 -23
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/components.js +26 -27
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/context.js +6 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/dom/render.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/dom/server.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/hooks/index.js +16 -13
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/intrinsic-element/components.js +4 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/streaming.js +5 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cache/index.js +103 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/compress/index.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cors/index.js +17 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/csrf/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/digest.js +47 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/index.js +7 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/jwk/jwk.js +9 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/jwt/jwt.js +9 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/language/language.js +10 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/method-not-allowed/index.js +68 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/pretty-json/index.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +16 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/request.js +21 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/linear-router/router.js +7 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/pattern-router/router.js +3 -9
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/reg-exp-router/node.js +61 -58
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/reg-exp-router/router.js +77 -129
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/reg-exp-router/trie.js +14 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/node.js +47 -70
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/router.js +3 -11
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/utils.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/aws-lambda/types.d.ts +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/client/types.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/context.d.ts +6 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/hono-base.d.ts +4 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/base.d.ts +7 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/dom/index.d.ts +5 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/dom/intrinsic-element/components.d.ts +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/dom/server.d.ts +5 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/hooks/index.d.ts +8 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/index.d.ts +5 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/cache/index.d.ts +6 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/combine/index.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/cors/index.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/jsx-renderer/index.d.ts +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/jwk/jwk.d.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/jwt/jwt.d.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/method-not-allowed/index.d.ts +49 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/secure-headers/permissions-policy.d.ts +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +4 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/reg-exp-router/trie.d.ts +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/trie-router/node.d.ts +1 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/trie-router/router.d.ts +0 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/utils.d.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/headers.d.ts +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/url.d.ts +5 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/accept.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/body.js +21 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/cookie.js +5 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/ipaddr.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/stream.js +12 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/url.js +17 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/package.json +11 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/README.md +173 -143
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/address-error.d.ts +11 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/address-error.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/common.d.ts +49 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/common.js +79 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/common.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv4.d.ts +78 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv4.js +119 -24
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv6.d.ts +151 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv6.js +316 -90
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v4/constants.d.ts +12 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v4/constants.js +45 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v4/constants.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v6/constants.d.ts +14 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v6/constants.js +50 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v6/constants.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/package.json +6 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/.github/THREAT_MODEL.md +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/CHANGELOG.md +26 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/README.md +19 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/dist/qs.js +25 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/eslint.config.mjs +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/lib/parse.js +16 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/lib/stringify.js +23 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/lib/utils.js +57 -11
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/package.json +8 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/test/parse.js +419 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/test/stringify.js +317 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/test/utils.js +206 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/CHANGELOG.md +10 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/README.md +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/index.js +5 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/package.json +10 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/test/index.js +16 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/index.js +8 -18
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/LICENSE +22 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/README.md +71 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/dist/index.d.ts +46 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/dist/index.js +176 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/package.json +52 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/package.json +9 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/package-lock.json +72 -41
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.package-lock.json +6 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/brace-expansion/README.md +23 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/brace-expansion/index.js +265 -86
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/README.md +173 -143
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/address-error.d.ts +11 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/address-error.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/common.d.ts +49 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/common.js +79 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/common.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv4.d.ts +78 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv4.js +119 -24
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv6.d.ts +151 -10
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv6.js +316 -90
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v4/constants.d.ts +12 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v4/constants.js +45 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v4/constants.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v6/constants.d.ts +14 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v6/constants.js +50 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v6/constants.js.map +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/package.json +6 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/package-lock.json +6 -6
- package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +18 -9
- package/node_modules/playwright/node_modules/playwright-core/lib/mcpBundleImpl/index.js +47 -47
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +3 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +42 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utilsBundleImpl/index.js +121 -121
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B7KbSQcC.js → index.BAkLd5DX.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/cli/program.ts +31 -9
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +57 -1
- package/node_modules/playwright/package.json +2 -2
- package/package.json +7 -6
- package/plugin/prompts/code-reuse/hand-off.md +30 -0
- package/plugin/prompts/generate-tests/generation.md +1 -1
- package/plugin/prompts/plan-tests.md +14 -14
- package/plugin/prompts/testbot-task1.md +2 -2
- package/plugin/skills/enhance-assertions/SKILL.md +25 -0
- package/plugin/skills/enhance-assertions/reference/contract.md +51 -0
- package/plugin/skills/enhance-assertions/reference/integration.md +58 -0
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +220 -0
- package/plugin/skills/enhance-assertions/reference/ui.md +373 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +45 -50
- package/plugin/skills/run-test/SKILL.md +16 -0
- package/plugin/skills/validate-assertion-alignment-post-execution/SKILL.md +32 -0
- package/plugin/skills/validate-assertion-alignment-post-execution/reference/checks.md +44 -0
- package/plugin/skills/validate-assertion-alignment-post-execution/reference/evidence.md +47 -0
- package/build/adapters/jestAdapter.d.ts +0 -14
- package/build/adapters/jestAdapter.js +0 -113
- package/build/adapters/mochaAdapter.d.ts +0 -13
- package/build/adapters/mochaAdapter.js +0 -87
- package/build/adapters/playwrightAdapter.d.ts +0 -17
- package/build/adapters/playwrightAdapter.js +0 -182
- package/build/adapters/pytestAdapter.d.ts +0 -15
- package/build/adapters/pytestAdapter.js +0 -108
- package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +0 -2
- package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.js +0 -29
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +0 -2
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +0 -36
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +0 -16
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +0 -284
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +0 -2
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +0 -388
- package/build/tools/runExistingTestsTool.d.ts +0 -138
- package/build/tools/runExistingTestsTool.js +0 -644
- package/build/types/ExternalTestExecution.d.ts +0 -67
- package/build/types/ExternalTestExecution.js +0 -8
- package/build/workspace/testSuites.d.ts +0 -20
- package/build/workspace/testSuites.js +0 -17
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { TestExecutionService } from "../services/TestExecutionService.js";
|
|
3
|
+
import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import * as fs from "fs";
|
|
5
7
|
import path from "path";
|
|
6
8
|
import { stripVTControlCharacters } from "util";
|
|
7
|
-
import { TestExecutionService } from "../services/TestExecutionService.js";
|
|
8
|
-
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
9
9
|
import { makeProgressReporter } from "../utils/progress.js";
|
|
10
|
+
import { pendingReuseDebt } from "./code-refactor/reuse-state.js";
|
|
11
|
+
import { assertionFeedbackForExecution, canonicalTestPath, recordAssertionExecution, } from "./code-refactor/assertion-state.js";
|
|
12
|
+
import { stageAndRecordRetrofits } from "./code-refactor/retrofit-state.js";
|
|
13
|
+
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
10
14
|
import { TestExecutionStatus, } from "../types/TestExecution.js";
|
|
11
|
-
import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
|
|
12
15
|
import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
|
|
13
|
-
import { StateManager, getTestsRepoDir, } from "../utils/AnalysisStateManager.js";
|
|
14
|
-
import { DriftAction,
|
|
16
|
+
import { StateManager, currentRunStateFile, getPrimaryRepository, getTestsRepoDir, resolveOwnRunStatePath, resolveRunStatePath, } from "../utils/AnalysisStateManager.js";
|
|
17
|
+
import { DriftAction, } from "../types/TestAnalysis.js";
|
|
15
18
|
import { logger } from "../utils/logger.js";
|
|
16
19
|
import { toolError } from "../utils/utils.js";
|
|
17
20
|
import { recordExecutionVideo } from "./execution-video-state.js";
|
|
18
21
|
import { stageGeneratedPaths } from "../utils/gitStaging.js";
|
|
19
|
-
import
|
|
22
|
+
import { walkDir } from "../utils/fileWalk.js";
|
|
23
|
+
import { findRepoPlaywrightConfig, writeWrapperConfig, commandPassesBrowserFlag, } from "../execution/wrapperConfig.js";
|
|
24
|
+
import { canonicalStateFilePath, persistTestExecutionResult, readPinnedMaxFixAttempts, readRepoSectionOrThrow, reserveTestExecutionAttempt, stateFileKey, } from "../utils/testExecutionRecord.js";
|
|
25
|
+
import { getMaxFixAttempts } from "../utils/fixAttempts.js";
|
|
26
|
+
import { runSerialized } from "../utils/runSerialized.js";
|
|
27
|
+
import { sha256Of } from "../utils/assertion-verify/index.js";
|
|
20
28
|
import { baselineFileMatchesStem, baselineStem, rebaselineSnapshotNameSchema, snapshotDirFor, } from "../utils/rebaselineSnapshots.js";
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
29
|
+
export const TOOL_NAME = "skyramp_execute_test";
|
|
30
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
31
|
+
const MAX_TIMEOUT_MS = 3_600_000;
|
|
32
|
+
/** Output kept per run, from its end: it is held in memory, saved to the state file, and returned to the agent. */
|
|
33
|
+
export const MAX_OUTPUT_CHARS = 200_000;
|
|
24
34
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
35
|
+
* `unauthenticated: true` forces no token, so the child env carries no
|
|
36
|
+
* SKYRAMP_TEST_TOKEN at all — an empty Authorization header triggers encoding
|
|
37
|
+
* errors on unauthenticated endpoints (E7). An empty `token` means "use the
|
|
38
|
+
* server's environment", as the local-dev prompt passes it.
|
|
29
39
|
*/
|
|
30
40
|
export function resolveEffectiveToken(unauthenticated, paramToken, envToken) {
|
|
31
41
|
if (unauthenticated)
|
|
32
42
|
return "";
|
|
33
|
-
return paramToken
|
|
34
|
-
}
|
|
35
|
-
export function shouldInjectSkyrampBaseUrl(testType, contractMode) {
|
|
36
|
-
return testType !== TestType.CONTRACT || contractMode !== "consumer";
|
|
43
|
+
return paramToken || envToken || "";
|
|
37
44
|
}
|
|
38
45
|
/**
|
|
39
46
|
* Append the recorded video path to execution output.
|
|
@@ -46,6 +53,242 @@ export function shouldInjectSkyrampBaseUrl(testType, contractMode) {
|
|
|
46
53
|
export function withVideoInfo(output, videoPath) {
|
|
47
54
|
return videoPath ? `${output}\n\nVideo recording: ${videoPath}` : output;
|
|
48
55
|
}
|
|
56
|
+
function isBrowserTest(testType) {
|
|
57
|
+
return testType === TestType.UI || testType === TestType.E2E;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One directory per run: a unique name leaves nothing to clean up between runs, and
|
|
61
|
+
* the random suffix keeps two runs in the same millisecond apart.
|
|
62
|
+
*/
|
|
63
|
+
export function videoSubdirName(testFile) {
|
|
64
|
+
const basename = path
|
|
65
|
+
.basename(testFile, path.extname(testFile))
|
|
66
|
+
.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
67
|
+
const hash = crypto
|
|
68
|
+
.createHash("sha256")
|
|
69
|
+
.update(testFile)
|
|
70
|
+
.digest("hex")
|
|
71
|
+
.slice(0, 8);
|
|
72
|
+
return `${basename}-${hash}-${Date.now()}-${crypto.randomBytes(3).toString("hex")}`;
|
|
73
|
+
}
|
|
74
|
+
/** The first `video.webm` under a run's video directory, if one was recorded. */
|
|
75
|
+
export function collectVideoPath(videoDir) {
|
|
76
|
+
try {
|
|
77
|
+
for (const [entry, fullPath] of walkDir(videoDir)) {
|
|
78
|
+
if (entry.name === "video.webm")
|
|
79
|
+
return fullPath;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
logger.warning(`Could not scan ${videoDir} for a video`, {
|
|
84
|
+
error: String(err),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The directory whose `.skyramp/workspace.yml` describes this run: the nearest
|
|
91
|
+
* ancestor of `cwd` that has one, else the run's primary checkout (a tests repo
|
|
92
|
+
* delivered apart from the SUT has no workspace.yml of its own), else `cwd`.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveWorkspaceRoot(cwd) {
|
|
95
|
+
let dir = path.resolve(cwd);
|
|
96
|
+
for (;;) {
|
|
97
|
+
if (fs.existsSync(path.join(dir, ".skyramp", "workspace.yml")))
|
|
98
|
+
return dir;
|
|
99
|
+
const parent = path.dirname(dir);
|
|
100
|
+
if (parent === dir)
|
|
101
|
+
break;
|
|
102
|
+
dir = parent;
|
|
103
|
+
}
|
|
104
|
+
return getPrimaryRepository()?.repositoryPath ?? path.resolve(cwd);
|
|
105
|
+
}
|
|
106
|
+
/** pytest splits PYTEST_ADDOPTS with shlex, so a path with whitespace needs quotes. */
|
|
107
|
+
export function appendPytestVideoOpts(existing, videoDir) {
|
|
108
|
+
const dir = /\s/.test(videoDir) ? JSON.stringify(videoDir) : videoDir;
|
|
109
|
+
return [existing?.trim(), `--video on --output ${dir}`]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join(" ");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The names by which a command can select `testFile`: its basename, and for Java the
|
|
115
|
+
* class name, because Maven selects a test with `-Dtest=FooTest`.
|
|
116
|
+
*/
|
|
117
|
+
export function testFileNames(testFile) {
|
|
118
|
+
const basename = path.basename(testFile);
|
|
119
|
+
return path.extname(basename) === ".java"
|
|
120
|
+
? [basename, path.basename(basename, ".java")]
|
|
121
|
+
: [basename];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Backslashes are ignored: Playwright and Jest read the path as a regular
|
|
125
|
+
* expression, so the agent escapes it (`a\.spec\.ts`).
|
|
126
|
+
*
|
|
127
|
+
* A trailing shell comment is removed first. `npm test # a_test.py` names the
|
|
128
|
+
* file only in a comment and would run the whole suite, and the check exists
|
|
129
|
+
* to stop exactly that. This is a name check, not a parse: a command that
|
|
130
|
+
* mentions the file in some other way it does not run still passes.
|
|
131
|
+
*/
|
|
132
|
+
/** The config an explicit `--config <path>` names, resolved against `cwd`, or
|
|
133
|
+
* undefined when the command carries none. `$SKYRAMP_PLAYWRIGHT_CONFIG` is
|
|
134
|
+
* ours and is not a repository config. */
|
|
135
|
+
export function explicitPlaywrightConfig(command, cwd) {
|
|
136
|
+
const m = /--config[= ]\s*("[^"]+"|'[^']+'|[^\s]+)/.exec(command);
|
|
137
|
+
if (!m)
|
|
138
|
+
return undefined;
|
|
139
|
+
const raw = m[1].replace(/^["']|["']$/g, "");
|
|
140
|
+
if (raw.includes("SKYRAMP_PLAYWRIGHT_CONFIG"))
|
|
141
|
+
return undefined;
|
|
142
|
+
const resolved = path.isAbsolute(raw) ? raw : path.join(cwd, raw);
|
|
143
|
+
return fs.existsSync(resolved) ? resolved : undefined;
|
|
144
|
+
}
|
|
145
|
+
/** The command with its `--config <repo config>` pointed at the wrapper. The
|
|
146
|
+
* wrapper imports that same config, so the run keeps the repository's testDir
|
|
147
|
+
* and projects and gains the video overlay. Without this swap a command that
|
|
148
|
+
* names a config runs the config directly and records nothing. */
|
|
149
|
+
export function pointConfigAtWrapper(command, wrapperPath) {
|
|
150
|
+
return command.replace(/(--config[= ]\s*)("[^"]+"|'[^']+'|[^\s]+)/, (_m, flag) => `${flag}${JSON.stringify(wrapperPath)}`);
|
|
151
|
+
}
|
|
152
|
+
export function commandNamesTestFile(command, testFile) {
|
|
153
|
+
const withoutComment = command.replace(/(^|\s)#.*$/, "$1");
|
|
154
|
+
const unescaped = withoutComment.replace(/\\/g, "");
|
|
155
|
+
return testFileNames(testFile).some((name) => unescaped.includes(name));
|
|
156
|
+
}
|
|
157
|
+
/** pytest exits 4 on a usage error; without pytest-playwright `--video` is one. */
|
|
158
|
+
export function pytestRejectedVideoOptions(run) {
|
|
159
|
+
return (run.exitCode === 4 &&
|
|
160
|
+
/unrecognized arguments:[^\n]*--video/.test(run.output));
|
|
161
|
+
}
|
|
162
|
+
/** What the output says when the command ended before any test ran. Each entry
|
|
163
|
+
* is the line the runner prints instead of a result: a missing package, or a
|
|
164
|
+
* file the runner never matched. Node, Playwright and Jest all exit 1 for
|
|
165
|
+
* these, so without this the run is recorded as a failing test. */
|
|
166
|
+
const NO_RUN_PATTERNS = [
|
|
167
|
+
/^.*\bError: Cannot find module\b.*$/m,
|
|
168
|
+
/^Cannot find module\b.*$/m,
|
|
169
|
+
/^.*\bERR_MODULE_NOT_FOUND\b.*$/m,
|
|
170
|
+
/^(?:Error: )?No tests found\b.*$/im,
|
|
171
|
+
// npx --no-install refuses to fetch a runner the repository does not have.
|
|
172
|
+
/^.*\bnpx canceled due to missing packages\b.*$/m,
|
|
173
|
+
// Jest matched the file and loaded it, and it declared no test.
|
|
174
|
+
/^.*\bYour test suite must contain at least one test\b.*$/m,
|
|
175
|
+
];
|
|
176
|
+
/** The line proving no test ran, or undefined when the output does not say so. */
|
|
177
|
+
export function noTestRanReason(output) {
|
|
178
|
+
for (const pattern of NO_RUN_PATTERNS) {
|
|
179
|
+
const hit = pattern.exec(output);
|
|
180
|
+
if (hit)
|
|
181
|
+
return hit[0].trim();
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
export function verdictForExit(exitCode, timedOut, output = "") {
|
|
186
|
+
if (timedOut)
|
|
187
|
+
return TestExecutionStatus.Error;
|
|
188
|
+
if (exitCode === 0)
|
|
189
|
+
return TestExecutionStatus.Pass;
|
|
190
|
+
// Exit 1 is the failing-test code, but it is also what a runner returns when
|
|
191
|
+
// it never got as far as a test. Error keeps those out of the failing count.
|
|
192
|
+
if (exitCode === 1)
|
|
193
|
+
return noTestRanReason(output)
|
|
194
|
+
? TestExecutionStatus.Error
|
|
195
|
+
: TestExecutionStatus.Fail;
|
|
196
|
+
return TestExecutionStatus.Error;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* How long output may keep arriving after the shell exits. A background process the
|
|
200
|
+
* command started holds the pipes open, so waiting for them to close would wait for it.
|
|
201
|
+
*/
|
|
202
|
+
const EXIT_OUTPUT_GRACE_MS = 2_000;
|
|
203
|
+
/**
|
|
204
|
+
* Runs `command` through the shell in its own process group, so a timeout kills
|
|
205
|
+
* the runner's children too. stdout and stderr share one buffer in arrival order.
|
|
206
|
+
*/
|
|
207
|
+
export function spawnTestCommand(opts) {
|
|
208
|
+
const startedAt = Date.now();
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
let output = "";
|
|
211
|
+
let timedOut = false;
|
|
212
|
+
let spawnError;
|
|
213
|
+
let exitCode = null;
|
|
214
|
+
let settled = false;
|
|
215
|
+
let grace;
|
|
216
|
+
const child = spawn(opts.command, {
|
|
217
|
+
cwd: opts.cwd,
|
|
218
|
+
env: opts.env,
|
|
219
|
+
shell: true,
|
|
220
|
+
detached: process.platform !== "win32",
|
|
221
|
+
});
|
|
222
|
+
const killGroup = () => {
|
|
223
|
+
try {
|
|
224
|
+
if (process.platform !== "win32" && child.pid) {
|
|
225
|
+
process.kill(-child.pid, "SIGKILL");
|
|
226
|
+
}
|
|
227
|
+
else if (child.pid) {
|
|
228
|
+
// Windows has no process group: child.kill reaches the shell and
|
|
229
|
+
// leaves the runner beneath it alive, still writing to the checkout.
|
|
230
|
+
// /T takes the tree, /F forces it.
|
|
231
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
232
|
+
stdio: "ignore",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// already gone
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const finish = () => {
|
|
241
|
+
if (settled)
|
|
242
|
+
return;
|
|
243
|
+
settled = true;
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
clearTimeout(grace);
|
|
246
|
+
if (output.length > MAX_OUTPUT_CHARS) {
|
|
247
|
+
output = output.slice(-MAX_OUTPUT_CHARS);
|
|
248
|
+
truncated = true;
|
|
249
|
+
}
|
|
250
|
+
resolve({
|
|
251
|
+
exitCode,
|
|
252
|
+
output: (truncated
|
|
253
|
+
? `[output truncated to its last ${MAX_OUTPUT_CHARS} characters]\n`
|
|
254
|
+
: "") + stripVTControlCharacters(output),
|
|
255
|
+
timedOut,
|
|
256
|
+
spawnError,
|
|
257
|
+
duration: Date.now() - startedAt,
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const timer = setTimeout(() => {
|
|
261
|
+
timedOut = true;
|
|
262
|
+
killGroup();
|
|
263
|
+
}, opts.timeoutMs);
|
|
264
|
+
let truncated = false;
|
|
265
|
+
const append = (d) => {
|
|
266
|
+
output += String(d);
|
|
267
|
+
if (output.length > 2 * MAX_OUTPUT_CHARS) {
|
|
268
|
+
output = output.slice(-MAX_OUTPUT_CHARS);
|
|
269
|
+
truncated = true;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
child.stdout?.on("data", append);
|
|
273
|
+
child.stderr?.on("data", append);
|
|
274
|
+
child.on("error", (err) => {
|
|
275
|
+
spawnError = String(err);
|
|
276
|
+
finish();
|
|
277
|
+
});
|
|
278
|
+
child.on("exit", (code) => {
|
|
279
|
+
exitCode = code;
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
grace = setTimeout(() => {
|
|
282
|
+
killGroup();
|
|
283
|
+
finish();
|
|
284
|
+
}, EXIT_OUTPUT_GRACE_MS);
|
|
285
|
+
});
|
|
286
|
+
child.on("close", (code) => {
|
|
287
|
+
exitCode = code ?? exitCode;
|
|
288
|
+
finish();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
49
292
|
/**
|
|
50
293
|
* Where a real HTTP 401 shows up in runner output. Every entry is a shape taken from
|
|
51
294
|
* actual skyramp_execute_test output in the eval logs, not from guesswork:
|
|
@@ -76,7 +319,6 @@ const HTTP_401_SHAPES = [
|
|
|
76
319
|
// there, while a test TITLE ("should return 401 Unauthorized for an expired
|
|
77
320
|
// token") always has words in front of it and is not evidence of a response.
|
|
78
321
|
/^\s*401\s+unauthori[sz]ed\b/im,
|
|
79
|
-
// Status line, as curl -i and Go's httputil print it.
|
|
80
322
|
/\bHTTP\/[\d.]+\s+401\b/i,
|
|
81
323
|
// A status FIELD set to 401 — the value the response carried. Only `:` is
|
|
82
324
|
// accepted: `== 401` and `= 401` are an assertion or echoed test source, which
|
|
@@ -85,8 +327,6 @@ const HTTP_401_SHAPES = [
|
|
|
85
327
|
// (`Expected: {"status": 401}`) alongside `Received: {"status": 500}` is not a
|
|
86
328
|
// 401 the app sent. Reject the line rather than the value.
|
|
87
329
|
/^(?!.*\bexpected\b).*\b(?:status|status[_-]?code|statuscode|code|errorcode)\\?"?\s*:\s*401\b/im,
|
|
88
|
-
// Playwright prints both compared values. `Received` is what the app sent;
|
|
89
|
-
// `Expected` is what the test wanted, so it is not evidence of a 401.
|
|
90
330
|
/^\s*Received:\s*401\b/im,
|
|
91
331
|
// pytest assertion rewriting. A Skyramp-generated test reads
|
|
92
332
|
// `assert response.status_code == N`, so the OBSERVED value is on the left and
|
|
@@ -119,14 +359,10 @@ export function resolveRebaselineSnapshots(requested, phase) {
|
|
|
119
359
|
return { snapshots };
|
|
120
360
|
}
|
|
121
361
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* `<stem>-
|
|
125
|
-
*
|
|
126
|
-
* the tool can tell the agent which baselines were actually rewritten — SmartPlaywright
|
|
127
|
-
* is the only party that knows the exact filename, and an executor image that lacks
|
|
128
|
-
* SKYRAMP_UPDATE_SNAPSHOTS (or a name matching no toHaveScreenshot call) leaves
|
|
129
|
-
* every file untouched.
|
|
362
|
+
* For each requested name, every PNG under `<spec>-snapshots/` whose name matches the
|
|
363
|
+
* stem, with size and mtime. A spec may hold both `<stem>-linux.png` and
|
|
364
|
+
* `<stem>-chromium-linux.png`; latching onto one would misreport the other. Taken
|
|
365
|
+
* before and after the run to tell which baselines were actually rewritten.
|
|
130
366
|
*/
|
|
131
367
|
export function readBaselineState(specFile, requested) {
|
|
132
368
|
const dir = snapshotDirFor(specFile);
|
|
@@ -154,10 +390,6 @@ export function readBaselineState(specFile, requested) {
|
|
|
154
390
|
}
|
|
155
391
|
return state;
|
|
156
392
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Which requested baselines changed on disk between two readBaselineState calls, and
|
|
159
|
-
* which files carried the change (the ones to stage).
|
|
160
|
-
*/
|
|
161
393
|
export function diffBaselineState(before, after) {
|
|
162
394
|
const refreshed = [];
|
|
163
395
|
const notRefreshed = [];
|
|
@@ -193,7 +425,11 @@ export function diffBaselineState(before, after) {
|
|
|
193
425
|
export function authorizeRebaseline(stateData, testFile, requested) {
|
|
194
426
|
if (requested.length === 0)
|
|
195
427
|
return {};
|
|
196
|
-
|
|
428
|
+
// Canonical on both sides: the verdict and the row were written by analysis,
|
|
429
|
+
// the argument by the agent, and `/repo/tests/./a.spec.ts` is one file.
|
|
430
|
+
const key = canonicalTestPath(testFile);
|
|
431
|
+
const verdict = (stateData?.maintenanceVerdicts ?? []).find((v) => canonicalTestPath(String(v.testFilePath ?? "")) === key &&
|
|
432
|
+
v.action === DriftAction.Update);
|
|
197
433
|
if (!verdict) {
|
|
198
434
|
return {
|
|
199
435
|
error: `rebaselineSnapshots refused: no UPDATE verdict for ${testFile} in the stateFile. Only a maintained test whose skyramp_analyze_test_health verdict listed the baselines can be refreshed — a newly generated test writes its own baseline on its first run and has nothing to refresh.`,
|
|
@@ -206,8 +442,12 @@ export function authorizeRebaseline(stateData, testFile, requested) {
|
|
|
206
442
|
error: `rebaselineSnapshots refused: ${unauthorized.join(", ")} not in the UPDATE verdict for ${path.basename(testFile)} (authorized: ${authorized.size ? [...authorized].join(", ") : "none"}). Pass exactly the rebaseline_snapshots skyramp_actions returned, using the name the test passes to toHaveScreenshot (page-001.png), not the on-disk file (page-001-chromium-linux.png).`,
|
|
207
443
|
};
|
|
208
444
|
}
|
|
209
|
-
const entry = (stateData?.existingTests ?? []).find((t) => t.testFile ===
|
|
210
|
-
|
|
445
|
+
const entry = (stateData?.existingTests ?? []).find((t) => canonicalTestPath(t.testFile) === key);
|
|
446
|
+
// The reservation writes an Unknown placeholder into this row while a before
|
|
447
|
+
// run is in flight (SKYR-4460); a run that died there left no evidence, so
|
|
448
|
+
// the placeholder must not read as a recorded baseline.
|
|
449
|
+
if (!entry?.executionBefore ||
|
|
450
|
+
entry.executionBefore.status === TestExecutionStatus.Unknown) {
|
|
211
451
|
return {
|
|
212
452
|
error: `rebaselineSnapshots refused: no phase: "before" execution is recorded for ${path.basename(testFile)}. Run the pre-edit baseline first (it is the evidence the baseline is stale), then pass rebaselineSnapshots on the final run.`,
|
|
213
453
|
};
|
|
@@ -223,30 +463,33 @@ export function authorizeRebaseline(stateData, testFile, requested) {
|
|
|
223
463
|
return {};
|
|
224
464
|
}
|
|
225
465
|
/**
|
|
226
|
-
* Reconcile the persisted verdict with what the
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* rebaseline-only UPDATE with nothing left becomes VERIFY (the test stays red, the
|
|
232
|
-
* rationale says why), and an UPDATE that also carried edits keeps UPDATE and is
|
|
233
|
-
* held to its edit. The report then reflects what happened, not what was asked.
|
|
466
|
+
* Reconcile the persisted verdict with what the run actually did (SKYR-4298). A
|
|
467
|
+
* @skyramp/skyramp that predates SKYRAMP_UPDATE_SNAPSHOTS rewrites nothing; left
|
|
468
|
+
* alone, the verdict would still promise a refresh and the report gate would refuse
|
|
469
|
+
* the report. Names not refreshed are dropped; a rebaseline-only UPDATE with nothing
|
|
470
|
+
* left becomes VERIFY, and an UPDATE that also carried edits is held to its edit.
|
|
234
471
|
*/
|
|
235
|
-
export function applyRefreshOutcomeToVerdicts(verdicts, testFile, outcome
|
|
472
|
+
export function applyRefreshOutcomeToVerdicts(verdicts, testFile, outcome) {
|
|
236
473
|
if (outcome.notRefreshed.length === 0)
|
|
237
474
|
return { verdicts };
|
|
238
475
|
let note;
|
|
239
476
|
const next = verdicts.map((v) => {
|
|
240
|
-
if (v.testFilePath
|
|
477
|
+
if (canonicalTestPath(String(v.testFilePath ?? "")) !==
|
|
478
|
+
canonicalTestPath(testFile) ||
|
|
479
|
+
v.action !== DriftAction.Update)
|
|
241
480
|
return v;
|
|
242
481
|
const remaining = (v.rebaselineSnapshots ?? []).filter((n) => !outcome.notRefreshed.includes(n));
|
|
243
|
-
const reason = `baseline refresh of ${outcome.notRefreshed.join(", ")} was not applied by
|
|
482
|
+
const reason = `baseline refresh of ${outcome.notRefreshed.join(", ")} was not applied by the test run (its @skyramp/skyramp lacks SKYRAMP_UPDATE_SNAPSHOTS, or the name matches no toHaveScreenshot call)`;
|
|
244
483
|
if (remaining.length === 0 && v.rebaselineOnly) {
|
|
245
484
|
note = `Verdict for ${path.basename(testFile)} downgraded UPDATE → VERIFY: ${reason}. The test stays as it is; report it honestly.`;
|
|
246
485
|
const { rebaselineSnapshots: _dropped, rebaselineOnly: _only, ...rest } = v;
|
|
247
486
|
void _dropped;
|
|
248
487
|
void _only;
|
|
249
|
-
return {
|
|
488
|
+
return {
|
|
489
|
+
...rest,
|
|
490
|
+
action: DriftAction.Verify,
|
|
491
|
+
rationale: `${v.rationale} [${reason}]`,
|
|
492
|
+
};
|
|
250
493
|
}
|
|
251
494
|
note = `Verdict for ${path.basename(testFile)}: ${reason}; the UPDATE is held to its spec/POM edit only.`;
|
|
252
495
|
const { rebaselineSnapshots: _dropped, ...rest } = v;
|
|
@@ -271,11 +514,82 @@ export function describeRefreshOutcome(outcome) {
|
|
|
271
514
|
parts.push(`Visual baselines refreshed: ${outcome.refreshed.join(", ")}.`);
|
|
272
515
|
}
|
|
273
516
|
if (outcome.notRefreshed.length > 0) {
|
|
274
|
-
parts.push(`Visual baselines NOT refreshed: ${outcome.notRefreshed.join(", ")} — the
|
|
517
|
+
parts.push(`Visual baselines NOT refreshed: ${outcome.notRefreshed.join(", ")} — the test's @skyramp/skyramp may lack SKYRAMP_UPDATE_SNAPSHOTS support, or the name matches no toHaveScreenshot() call in this spec. Do not report these as refreshed.`);
|
|
275
518
|
}
|
|
276
519
|
return parts.join(" ");
|
|
277
520
|
}
|
|
278
|
-
|
|
521
|
+
/**
|
|
522
|
+
/**
|
|
523
|
+
* The fix-and-rerun attempt cap (SKYR-4460), enforced where the prompt's prose
|
|
524
|
+
* cannot be: a file that has already received `cap` runs in the final phase
|
|
525
|
+
* gets a refusal instead of another execution. Only the "after" phase counts —
|
|
526
|
+
* a maintained test's baseline run is evidence, not an attempt — and a record
|
|
527
|
+
* from before the counter existed reads as zero attempts.
|
|
528
|
+
*
|
|
529
|
+
* Deliberate limit: `phase: "before"` runs are never refused. The cap bounds
|
|
530
|
+
* the fix-and-rerun loop, which is a final-phase activity; a baseline is taken
|
|
531
|
+
* once per file by the prompt (and once per existing test by the baseline
|
|
532
|
+
* runner), so there is no loop to bound there. A runaway baseline loop would
|
|
533
|
+
* need its own guard — it is not this one.
|
|
534
|
+
*/
|
|
535
|
+
export function fixAttemptRefusal(record, phase, cap, testFile) {
|
|
536
|
+
if (phase !== "after")
|
|
537
|
+
return undefined;
|
|
538
|
+
const prior = record?.afterRuns ?? 0;
|
|
539
|
+
if (prior < cap)
|
|
540
|
+
return undefined;
|
|
541
|
+
return (`Attempt cap reached: ${path.basename(testFile)} has already been executed ` +
|
|
542
|
+
`${prior} time${prior === 1 ? "" : "s"} in the final phase, and the cap for this ` +
|
|
543
|
+
`run is ${cap} (maxFixAttempts). It was NOT run again. Do not edit and re-run it: ` +
|
|
544
|
+
// The report accepts Pass, Fail or Skipped: a run that errored, or one that
|
|
545
|
+
// recorded no result, is reported as Fail.
|
|
546
|
+
(record?.status === TestExecutionStatus.Pass
|
|
547
|
+
? `report it as status "Pass" (its last run passed)`
|
|
548
|
+
: record?.status && record.status !== TestExecutionStatus.Unknown
|
|
549
|
+
? `report it as status "Fail" (its last run ended ${record.status})`
|
|
550
|
+
: `its last run recorded no result, so the report will publish "Unknown" for it — describe the failure in its details`) +
|
|
551
|
+
` with the failure details already in your context, then move on. A failing test ` +
|
|
552
|
+
`that documents a real bug is a valid outcome; say in the report whether the ` +
|
|
553
|
+
`failure is the bug or the test, and that the attempt cap was reached.`);
|
|
554
|
+
}
|
|
555
|
+
/** The budget line appended to a failure, so the agent knows how many
|
|
556
|
+
* fix-and-rerun attempts remain for this file before it must report. */
|
|
557
|
+
/** Wait before the tool's single retry of a transient connection error.
|
|
558
|
+
* Module state with a test seam, so the handler test does not sleep 10s. */
|
|
559
|
+
let transientRetryDelayMs = 10_000;
|
|
560
|
+
export function setTransientRetryDelayForTests(ms) {
|
|
561
|
+
transientRetryDelayMs = ms;
|
|
562
|
+
}
|
|
563
|
+
/** Appended to the response when the tool re-ran a thrown transient connection
|
|
564
|
+
* error itself: two executions, one attempt. The prompt withholds the agent's
|
|
565
|
+
* own unchanged re-run when it sees this, so one attempt is never three runs. */
|
|
566
|
+
const TRANSIENT_RETRY_NOTE = `\n\nNote: the executor hit a transient connection error and re-ran this file once unchanged before answering; the two runs count as one attempt. Do not re-run it unchanged again — fix the cause the output names, or report it.`;
|
|
567
|
+
export function describeAttempt(priorAfterRuns, cap, opts = {}) {
|
|
568
|
+
const attempt = priorAfterRuns + 1;
|
|
569
|
+
const remaining = Math.max(0, cap - attempt);
|
|
570
|
+
const head = `Execution attempt ${attempt} of ${cap} for this file (maxFixAttempts=${cap}).`;
|
|
571
|
+
if (remaining === 0) {
|
|
572
|
+
return (`${head} No fix-and-rerun attempts remain: do not edit and re-run this file ` +
|
|
573
|
+
`again — report it as failed with these details.`);
|
|
574
|
+
}
|
|
575
|
+
const budget = `${head} ${remaining} fix-and-rerun attempt${remaining === 1 ? "" : "s"} remain` +
|
|
576
|
+
`${remaining === 1 ? "s" : ""}. `;
|
|
577
|
+
if (opts.unchangedRerun) {
|
|
578
|
+
// The file's contents did not move since the previous run: this WAS the
|
|
579
|
+
// one unchanged re-run the file gets. Say so instead of offering another.
|
|
580
|
+
return (budget +
|
|
581
|
+
`This run was an unchanged re-run of the previous one (same file contents), ` +
|
|
582
|
+
`so no further unchanged re-run is allowed for this file: change the test ` +
|
|
583
|
+
`file or its inputs to address the cause above, or report it.`);
|
|
584
|
+
}
|
|
585
|
+
return (budget +
|
|
586
|
+
`Re-run only after changing the test file or its ` +
|
|
587
|
+
`inputs to address the cause above. A file gets ONE unchanged re-run, for ` +
|
|
588
|
+
`either of two failures: a run that produced no output at all, or a ` +
|
|
589
|
+
`connection error — and not even that when this response carries the tool's ` +
|
|
590
|
+
`retry note, which means the tool has already spent it.`);
|
|
591
|
+
}
|
|
592
|
+
export function buildExecutionFailureText(result, opts = {}) {
|
|
279
593
|
const output = stripVTControlCharacters(result.output || "").trim();
|
|
280
594
|
const facts = [`status=${result.status}`];
|
|
281
595
|
if (typeof result.exitCode === "number")
|
|
@@ -288,22 +602,27 @@ export function buildExecutionFailureText(result) {
|
|
|
288
602
|
sections.push(output);
|
|
289
603
|
}
|
|
290
604
|
else if (errors.length > 0) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
sections.push("The executor captured no test output, so there are no per-test diagnostics. " +
|
|
295
|
-
"It did report the error below, which is the cause on record — use it. Do " +
|
|
296
|
-
"NOT infer anything the Errors line does not say, and leave the generated " +
|
|
297
|
-
"test unchanged.");
|
|
605
|
+
sections.push("The command printed no output, so there are no per-test diagnostics. " +
|
|
606
|
+
"The error below is the cause on record — use it. Do NOT infer anything " +
|
|
607
|
+
"the Errors line does not say, and leave the generated test unchanged.");
|
|
298
608
|
}
|
|
299
609
|
else {
|
|
300
|
-
sections.push("The
|
|
610
|
+
sections.push("The command printed no output, so this failure carries no diagnostics of " +
|
|
301
611
|
"its own and the cause cannot be determined from it. The test runner, the " +
|
|
302
612
|
"test process, or the application could each have died silently. Do NOT " +
|
|
303
613
|
"report the app as unreachable or misconfigured on this basis — nothing " +
|
|
304
614
|
"here shows that. Check any Errors line below and that the test runner " +
|
|
305
|
-
"started, then
|
|
306
|
-
|
|
615
|
+
"started, then " +
|
|
616
|
+
(opts.unchangedRerun
|
|
617
|
+
? "— this run was already an unchanged re-run of the previous one, so do " +
|
|
618
|
+
"NOT re-run it again — "
|
|
619
|
+
: "re-run it once unchanged if the attempt budget still allows it and this " +
|
|
620
|
+
"response carries no retry note — a run that produced no output is one " +
|
|
621
|
+
"of the two cases an unchanged re-run is right (the other is a " +
|
|
622
|
+
"connection error), and neither applies once the tool has already " +
|
|
623
|
+
"re-run the file itself; if it fails the same way, ") +
|
|
624
|
+
"report the execution failure with its cause unresolved and leave the " +
|
|
625
|
+
"generated test unchanged.");
|
|
307
626
|
}
|
|
308
627
|
if (errors.length > 0)
|
|
309
628
|
sections.push(`Errors: ${errors.join("; ")}`);
|
|
@@ -317,357 +636,664 @@ export function buildExecutionFailureText(result) {
|
|
|
317
636
|
}
|
|
318
637
|
return sections.join("\n\n");
|
|
319
638
|
}
|
|
639
|
+
export const inputSchema = {
|
|
640
|
+
commandOverride: z
|
|
641
|
+
.string()
|
|
642
|
+
.trim()
|
|
643
|
+
.min(1)
|
|
644
|
+
.optional()
|
|
645
|
+
.describe("The shell command that runs this one test file with the repository's own test runner, run on this host. Prefer it: the test then runs the way the repository's own CI runs it. Take the command from the suite's testRunCommand in .skyramp/workspace.yml, a Makefile target, a package script, or the command the repository's CI runs, and limit it to this one file. When workspace.yml records more than one suite, take it from the suite whose pathGlobs match the file: another suite's runner either fails to load it or reports a result for a file it never ran. If the repository has none, use the framework's default: `npx --no-install <runner> <file>` for playwright, jest, vitest or mocha, so a runner the repository does not have fails the run instead of downloading a different version; `python -m pytest <file> -q` with the interpreter the repository's own tests use; `mvn -q test -Dtest=<class> -DfailIfNoTests=false` for junit. It must name the test file; a command that does not is refused without running. For a TypeScript or JavaScript ui or e2e test, the server sets SKYRAMP_PLAYWRIGHT_CONFIG to a config that records video. Pass it even when you expect the runner or a package to be missing: the failure output is what names what is missing, and a check you make before the run reaches no repair. Omit it to run the test in the Skyramp executor container instead, which needs workspacePath — that is the fallback for a repository this host cannot run, not the first choice."),
|
|
646
|
+
cwd: z
|
|
647
|
+
.string()
|
|
648
|
+
.optional()
|
|
649
|
+
.describe("Absolute path of the directory commandOverride runs in. Required with commandOverride."),
|
|
650
|
+
workspacePath: z
|
|
651
|
+
.string()
|
|
652
|
+
.optional()
|
|
653
|
+
.describe("Absolute path of the Skyramp workspace. Required when commandOverride is omitted, because the executor resolves the service and its base URL from it."),
|
|
654
|
+
testFile: z.string().describe("Absolute path to the test file to execute."),
|
|
655
|
+
language: z
|
|
656
|
+
.nativeEnum(ProgrammingLanguage)
|
|
657
|
+
.describe("Programming language of the test file to execute (e.g., python, javascript, typescript, java)"),
|
|
658
|
+
testType: z
|
|
659
|
+
.nativeEnum(TestType)
|
|
660
|
+
.describe("Type of the test to execute. Note: 'mock' is NOT a valid test type — mock files are deployed via their apply_mock() function, not executed as tests."),
|
|
661
|
+
phase: z
|
|
662
|
+
.enum(["before", "after"])
|
|
663
|
+
.default("after")
|
|
664
|
+
.describe("Execution phase for maintained tests: 'before' captures pre-edit baseline; 'after' (default) records post-edit result."),
|
|
665
|
+
stateFile: z
|
|
666
|
+
.string()
|
|
667
|
+
.optional()
|
|
668
|
+
.describe("Path to state file from skyramp_analyze_changes. Always pass when available — results are written back so skyramp_submit_report can override before/afterStatus with ground-truth pass/fail."),
|
|
669
|
+
repository: z
|
|
670
|
+
.string()
|
|
671
|
+
.trim()
|
|
672
|
+
.min(1)
|
|
673
|
+
.describe("The owner/repo whose analysis section to write execution results into (e.g. 'letsramp/api-insight'). Set it on every call — the primary's owner/repo for a primary-repo test, or a related repo's owner/repo for that repo's section of the run-scoped stateFile."),
|
|
674
|
+
token: z
|
|
675
|
+
.string()
|
|
676
|
+
.optional()
|
|
677
|
+
.describe("Explicit authentication token, set as SKYRAMP_TEST_TOKEN. Omit it, or pass an empty string, to use SKYRAMP_TEST_TOKEN from the server's environment. Use `unauthenticated: true` for no auth."),
|
|
678
|
+
unauthenticated: z
|
|
679
|
+
.boolean()
|
|
680
|
+
.optional()
|
|
681
|
+
.describe("Set true to force this test execution to carry NO auth token, even if SKYRAMP_TEST_TOKEN is set in the environment or a token was passed. Use for tests that must assert 401/403 unauthenticated behavior."),
|
|
682
|
+
rebaselineSnapshots: z
|
|
683
|
+
.array(rebaselineSnapshotNameSchema)
|
|
684
|
+
.optional()
|
|
685
|
+
.describe('UI tests only. toHaveScreenshot() baseline filenames (e.g. ["page-001.png"]) this run REPLACES instead of comparing against, because the PR intentionally changed how the captured page/element/region looks (SKYR-4298). ' +
|
|
686
|
+
"Pass exactly the list skyramp_actions returned as rebaseline_snapshots for this spec — the tool checks it against the persisted UPDATE verdict in stateFile (so stateFile is required) and refuses names the verdict did not authorize, a spec with no such verdict, or a spec whose phase: 'before' run has not been recorded yet. Use the name as the test passes it (page-001.png), not the on-disk file (page-001-chromium-linux.png). " +
|
|
687
|
+
"The refreshed PNGs land beside the spec and are delivered with the Testbot PR as image diffs; the result names which baselines were refreshed and which were not. Never use this to silence a screenshot mismatch the diff does not explain."),
|
|
688
|
+
timeout: z
|
|
689
|
+
.number()
|
|
690
|
+
.int()
|
|
691
|
+
.positive()
|
|
692
|
+
.max(MAX_TIMEOUT_MS, `timeout must be at most ${MAX_TIMEOUT_MS} ms`)
|
|
693
|
+
.optional()
|
|
694
|
+
.describe(`Milliseconds before the command is killed and the run recorded as Error. Default ${DEFAULT_TIMEOUT_MS}.`),
|
|
695
|
+
};
|
|
696
|
+
/** Returns a warning when the result, or its pre-edit baseline, was not saved. */
|
|
697
|
+
async function writeExecutionToState(params, result, reserved) {
|
|
698
|
+
if (!params.stateFile)
|
|
699
|
+
return undefined;
|
|
700
|
+
const where = `testFile ${params.testFile} in repository ${params.repository}`;
|
|
701
|
+
try {
|
|
702
|
+
const written = await persistTestExecutionResult(params.stateFile, params.repository, params.testType, params.phase, result,
|
|
703
|
+
// A reserved attempt already advanced the counters before the run.
|
|
704
|
+
{ reserved });
|
|
705
|
+
if (!written.saved) {
|
|
706
|
+
return `This result was not saved: the stateFile has no section for repository ${params.repository}.`;
|
|
707
|
+
}
|
|
708
|
+
if (params.phase === "before" && !written.matchedExistingTest) {
|
|
709
|
+
return `The phase: "before" result was not saved as a baseline: the stateFile lists no existing test for ${where}.`;
|
|
710
|
+
}
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
// The attempt was counted at reservation, so the count is right. The RESULT
|
|
715
|
+
// is what the report reads for afterStatus, so an unrecorded one has to
|
|
716
|
+
// reach the agent, not only the log.
|
|
717
|
+
return (`This result for ${where} was not saved to the stateFile: ${err.message}. ` +
|
|
718
|
+
`The attempt was counted; the report will read this file's result as Unknown ` +
|
|
719
|
+
`unless the state file is repaired.`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/** Stages rewritten baselines and reconciles the verdict; returns the text to append. */
|
|
723
|
+
async function settleRefresh(params, snapshots, before) {
|
|
724
|
+
const outcome = diffBaselineState(before, readBaselineState(params.testFile, snapshots));
|
|
725
|
+
let text = describeRefreshOutcome(outcome);
|
|
726
|
+
// The rewritten files themselves, never the directory: `git add` on the directory
|
|
727
|
+
// would ship anything else sitting there under one authorized baseline's authority.
|
|
728
|
+
for (const name of outcome.refreshed) {
|
|
729
|
+
for (const file of outcome.refreshedFiles[name] ?? []) {
|
|
730
|
+
try {
|
|
731
|
+
await stageGeneratedPaths(path.join(snapshotDirFor(params.testFile), file));
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
logger.warning(`Could not stage refreshed visual baseline ${file}: ${err.message}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (outcome.notRefreshed.length > 0 && params.stateFile) {
|
|
739
|
+
try {
|
|
740
|
+
const stateManager = StateManager.fromStatePath(params.stateFile);
|
|
741
|
+
const repo = params.repository;
|
|
742
|
+
const stateData = await stateManager.readRepoData(repo);
|
|
743
|
+
if (repo && stateData?.maintenanceVerdicts) {
|
|
744
|
+
const reconciled = applyRefreshOutcomeToVerdicts(stateData.maintenanceVerdicts, params.testFile, outcome);
|
|
745
|
+
await stateManager.updateRepoData({ ...stateData, maintenanceVerdicts: reconciled.verdicts }, { repo });
|
|
746
|
+
if (reconciled.note)
|
|
747
|
+
text += ` ${reconciled.note}`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
logger.warning(`Could not reconcile the maintenance verdict with the refresh outcome: ${err.message}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return text;
|
|
755
|
+
}
|
|
756
|
+
/** The fallback: no commandOverride, so the Skyramp executor container runs the
|
|
757
|
+
* test. Kept while host execution is proven — the container resolves the
|
|
758
|
+
* service and its base URL from the workspace, which the host path leaves to
|
|
759
|
+
* the agent's own command.
|
|
760
|
+
*
|
|
761
|
+
* It records the same result the host path records, through the same writer,
|
|
762
|
+
* so the report cannot tell which path produced a run. */
|
|
763
|
+
async function runInExecutor(params, snapshots, budget, onProgress, sendProgress) {
|
|
764
|
+
// runTest validated this for the executor mode before anything wrote state.
|
|
765
|
+
const workspacePath = params.workspacePath;
|
|
766
|
+
const token = resolveEffectiveToken(params.unauthenticated, params.token, process.env.SKYRAMP_TEST_TOKEN);
|
|
767
|
+
// Delivered tests and the state file sit outside workspacePath in a
|
|
768
|
+
// cross-repo run, so the executor needs the tests-repo root both to mount
|
|
769
|
+
// them and to match the service that owns the file.
|
|
770
|
+
const testRepoPath = getTestsRepoDir();
|
|
771
|
+
const { baseUrl, candidates, dockerNetwork } = await getWorkspaceBaseUrl(workspacePath, params.testFile, params.language, testRepoPath);
|
|
772
|
+
if (!baseUrl && candidates.length > 0) {
|
|
773
|
+
return toolError([
|
|
774
|
+
"Cannot determine SKYRAMP_TEST_BASE_URL — the test file matches more than one service:",
|
|
775
|
+
...candidates.map((c) => ` \u2022 ${c.serviceName}: ${c.baseUrl}`),
|
|
776
|
+
"",
|
|
777
|
+
"Set SKYRAMP_TEST_BASE_URL to the right service URL, or give each service its own testDirectory in .skyramp/workspace.yml.",
|
|
778
|
+
].join("\n"));
|
|
779
|
+
}
|
|
780
|
+
let injectedBaseUrl = false;
|
|
781
|
+
if (baseUrl && !process.env.SKYRAMP_TEST_BASE_URL) {
|
|
782
|
+
process.env.SKYRAMP_TEST_BASE_URL = baseUrl;
|
|
783
|
+
injectedBaseUrl = true;
|
|
784
|
+
}
|
|
785
|
+
const execOptions = {
|
|
786
|
+
testFile: params.testFile,
|
|
787
|
+
workspacePath,
|
|
788
|
+
testRepoPath,
|
|
789
|
+
language: params.language,
|
|
790
|
+
testType: params.testType,
|
|
791
|
+
token,
|
|
792
|
+
dockerNetwork,
|
|
793
|
+
useHostNetwork: false,
|
|
794
|
+
...(snapshots.length > 0 ? { rebaselineSnapshots: snapshots } : {}),
|
|
795
|
+
...(params.timeout !== undefined ? { timeout: params.timeout } : {}),
|
|
796
|
+
};
|
|
797
|
+
let result;
|
|
798
|
+
// The executor's own retry of a transient connection error: two executions
|
|
799
|
+
// billed as ONE attempt, on purpose — the SUT's hiccup is not the agent's fix
|
|
800
|
+
// to make. An EOF or a refused connection means the SUT is not ready yet, not
|
|
801
|
+
// that the test failed. Only a throw from the executor is retried here; a
|
|
802
|
+
// connection error inside the test's own output reaches the agent as a normal
|
|
803
|
+
// failure.
|
|
804
|
+
let transientRetried = false;
|
|
805
|
+
try {
|
|
806
|
+
const service = new TestExecutionService();
|
|
807
|
+
try {
|
|
808
|
+
result = await service.executeTest(execOptions, onProgress);
|
|
809
|
+
}
|
|
810
|
+
catch (firstErr) {
|
|
811
|
+
const errMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
812
|
+
if (!/\bEOF\b|connection refused|ECONNREFUSED|ECONNRESET/i.test(errMsg)) {
|
|
813
|
+
throw firstErr;
|
|
814
|
+
}
|
|
815
|
+
logger.info(`Test execution hit transient connection error, retrying after ${transientRetryDelayMs}ms...`, { error: errMsg });
|
|
816
|
+
await sendProgress(50, 100, "SUT connection error — retrying in 10s...");
|
|
817
|
+
await new Promise((r) => setTimeout(r, transientRetryDelayMs));
|
|
818
|
+
transientRetried = true;
|
|
819
|
+
try {
|
|
820
|
+
result = await service.executeTest(execOptions, onProgress);
|
|
821
|
+
}
|
|
822
|
+
catch (secondErr) {
|
|
823
|
+
// Both runs threw: the caller's catch owes the agent the retry note, or
|
|
824
|
+
// the prompt lets it spend an unchanged re-run the tool already used.
|
|
825
|
+
throw Object.assign(secondErr, { transientRetried: true });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
// Unset only what this call set; a caller's own value must survive.
|
|
831
|
+
if (injectedBaseUrl)
|
|
832
|
+
delete process.env.SKYRAMP_TEST_BASE_URL;
|
|
833
|
+
}
|
|
834
|
+
const warnings = [...result.warnings];
|
|
835
|
+
if (transientRetried) {
|
|
836
|
+
warnings.push("The executor hit a transient connection error and re-ran this file once unchanged before answering. This is the second run's result, counted as one attempt. Do not re-run it unchanged again — fix the cause the output names, or report it.");
|
|
837
|
+
}
|
|
838
|
+
const stateWarning = await writeExecutionToState(params, result, budget.reserved);
|
|
839
|
+
if (stateWarning)
|
|
840
|
+
warnings.push(stateWarning);
|
|
841
|
+
await recordExecutionVideo(result, params.stateFile);
|
|
842
|
+
const compose = (text) => [
|
|
843
|
+
withVideoInfo(text, result.videoPath),
|
|
844
|
+
...warnings.map((w) => `Warning: ${w}`),
|
|
845
|
+
]
|
|
846
|
+
.filter(Boolean)
|
|
847
|
+
.join("\n\n") + budget.noRunNote;
|
|
848
|
+
if (result.status !== TestExecutionStatus.Pass) {
|
|
849
|
+
return toolError(compose(buildExecutionFailureText(result, {
|
|
850
|
+
unchangedRerun: budget.unchangedRerun,
|
|
851
|
+
}) + budget.attemptLine));
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
content: [
|
|
855
|
+
{
|
|
856
|
+
type: "text",
|
|
857
|
+
text: compose(`Test execution passed in the executor (duration=${result.duration}ms).\n\n${result.output ?? ""}`),
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/** A throw after the attempt was reserved has spent it (a Docker image setup,
|
|
863
|
+
* a workspace check, a spawn that died). Land an Error result so the record is
|
|
864
|
+
* not left as the reservation placeholder, and tell the agent the attempt went. */
|
|
865
|
+
async function accountForThrow(params, err, budget) {
|
|
866
|
+
let persistNote = "";
|
|
867
|
+
if (budget.reserved) {
|
|
868
|
+
const warning = await writeExecutionToState(params, {
|
|
869
|
+
testFile: params.testFile,
|
|
870
|
+
status: TestExecutionStatus.Error,
|
|
871
|
+
executedAt: new Date().toISOString(),
|
|
872
|
+
duration: 0,
|
|
873
|
+
errors: [err.message],
|
|
874
|
+
warnings: [],
|
|
875
|
+
output: "",
|
|
876
|
+
}, true);
|
|
877
|
+
if (warning)
|
|
878
|
+
persistNote = `\n\nWarning: ${warning}`;
|
|
879
|
+
}
|
|
880
|
+
const retryNote = err.transientRetried
|
|
881
|
+
? TRANSIENT_RETRY_NOTE
|
|
882
|
+
: "";
|
|
883
|
+
return toolError(`Test execution failed: ${err.message}${retryNote}${budget.attemptLine}${persistNote}${budget.noRunNote}`);
|
|
884
|
+
}
|
|
885
|
+
/** The preferred path: the agent supplied the command, so the server runs it on
|
|
886
|
+
* this host in `cwd` and reads the verdict off the exit code. */
|
|
887
|
+
async function runOnHost(params, command, hostCwd, snapshots, budget) {
|
|
888
|
+
const warnings = [];
|
|
889
|
+
const invEnv = {};
|
|
890
|
+
const workspaceRoot = resolveWorkspaceRoot(hostCwd);
|
|
891
|
+
const token = resolveEffectiveToken(params.unauthenticated, params.token, process.env.SKYRAMP_TEST_TOKEN);
|
|
892
|
+
if (token)
|
|
893
|
+
invEnv.SKYRAMP_TEST_TOKEN = token;
|
|
894
|
+
else if (!params.unauthenticated) {
|
|
895
|
+
warnings.push("No auth token available — authenticated endpoints will likely return 401. Set SKYRAMP_TEST_TOKEN or pass token/unauthenticated.");
|
|
896
|
+
}
|
|
897
|
+
if (snapshots.length > 0) {
|
|
898
|
+
invEnv.SKYRAMP_UPDATE_SNAPSHOTS = snapshots.join(",");
|
|
899
|
+
}
|
|
900
|
+
let videoDir;
|
|
901
|
+
let wrapper;
|
|
902
|
+
/** The config the server picked because the command named none. */
|
|
903
|
+
let autoFoundConfig;
|
|
904
|
+
if (isBrowserTest(params.testType)) {
|
|
905
|
+
videoDir = path.join(workspaceRoot, ".skyramp", "videos", videoSubdirName(params.testFile));
|
|
906
|
+
fs.mkdirSync(videoDir, { recursive: true });
|
|
907
|
+
if (params.language === ProgrammingLanguage.PYTHON) {
|
|
908
|
+
invEnv.PYTEST_ADDOPTS = appendPytestVideoOpts(process.env.PYTEST_ADDOPTS, videoDir);
|
|
909
|
+
}
|
|
910
|
+
else if (params.language === ProgrammingLanguage.TYPESCRIPT ||
|
|
911
|
+
params.language === ProgrammingLanguage.JAVASCRIPT) {
|
|
912
|
+
const namedConfig = explicitPlaywrightConfig(command, hostCwd);
|
|
913
|
+
// A repository with one Playwright config per suite gets whichever sits
|
|
914
|
+
// nearest the test file, and its testDir may not collect that file. The
|
|
915
|
+
// agent cannot see the choice, so name it when the run finds no test.
|
|
916
|
+
autoFoundConfig = namedConfig
|
|
917
|
+
? undefined
|
|
918
|
+
: findRepoPlaywrightConfig(params.testFile, hostCwd);
|
|
919
|
+
wrapper = writeWrapperConfig({
|
|
920
|
+
// The wrapper replaces whatever --config the command carried, so an
|
|
921
|
+
// explicit one must be imported or the repository loses its projects,
|
|
922
|
+
// testDir and CI settings -- the thing the wrapper exists to keep.
|
|
923
|
+
repoConfigPath: namedConfig ?? autoFoundConfig,
|
|
924
|
+
fallbackDir: hostCwd,
|
|
925
|
+
outputDir: videoDir,
|
|
926
|
+
testFile: path.resolve(params.testFile),
|
|
927
|
+
commandPassesBrowserFlag: commandPassesBrowserFlag(command),
|
|
928
|
+
});
|
|
929
|
+
invEnv.SKYRAMP_PLAYWRIGHT_CONFIG = wrapper.path;
|
|
930
|
+
// A command that names its own config would otherwise load it directly and
|
|
931
|
+
// record no video, which is how a passing run loses its evidence.
|
|
932
|
+
if (namedConfig)
|
|
933
|
+
command = pointConfigAtWrapper(command, wrapper.path);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// testbot exports NODE_PATH=<mcp>/node_modules job-wide. Inheriting it lets the test
|
|
937
|
+
// load our @skyramp/skyramp and @playwright/test instead of the repository's own.
|
|
938
|
+
const env = { ...process.env, ...invEnv };
|
|
939
|
+
delete env.NODE_PATH;
|
|
940
|
+
if (!token)
|
|
941
|
+
delete env.SKYRAMP_TEST_TOKEN;
|
|
942
|
+
const baselinesBefore = snapshots.length > 0 ? readBaselineState(params.testFile, snapshots) : {};
|
|
943
|
+
const executedAt = new Date().toISOString();
|
|
944
|
+
const timeoutMs = params.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
945
|
+
let run;
|
|
946
|
+
try {
|
|
947
|
+
run = await spawnTestCommand({
|
|
948
|
+
command,
|
|
949
|
+
cwd: hostCwd,
|
|
950
|
+
env,
|
|
951
|
+
timeoutMs,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
// The wrapper sits in the customer's repo; a broad `git add` would ship it.
|
|
956
|
+
wrapper?.cleanup();
|
|
957
|
+
}
|
|
958
|
+
if (invEnv.PYTEST_ADDOPTS && pytestRejectedVideoOptions(run)) {
|
|
959
|
+
const plainEnv = { ...env };
|
|
960
|
+
if (process.env.PYTEST_ADDOPTS === undefined)
|
|
961
|
+
delete plainEnv.PYTEST_ADDOPTS;
|
|
962
|
+
else
|
|
963
|
+
plainEnv.PYTEST_ADDOPTS = process.env.PYTEST_ADDOPTS;
|
|
964
|
+
run = await spawnTestCommand({
|
|
965
|
+
command,
|
|
966
|
+
cwd: hostCwd,
|
|
967
|
+
env: plainEnv,
|
|
968
|
+
timeoutMs,
|
|
969
|
+
});
|
|
970
|
+
warnings.push("pytest did not recognize the added --video and --output options, so the test ran again without them. No video was recorded.");
|
|
971
|
+
videoDir = undefined;
|
|
972
|
+
}
|
|
973
|
+
const errors = [];
|
|
974
|
+
if (run.timedOut)
|
|
975
|
+
errors.push(`the command timed out after ${timeoutMs} ms`);
|
|
976
|
+
if (run.spawnError)
|
|
977
|
+
errors.push(run.spawnError);
|
|
978
|
+
const videoPath = videoDir ? collectVideoPath(videoDir) : undefined;
|
|
979
|
+
if (videoDir && !videoPath) {
|
|
980
|
+
warnings.push(`No video.webm was recorded under ${videoDir}. The verdict stands; the report has no video for this test.`);
|
|
981
|
+
}
|
|
982
|
+
const noRunReason = run.exitCode === 1 ? noTestRanReason(run.output) : undefined;
|
|
983
|
+
if (noRunReason) {
|
|
984
|
+
warnings.push(`No test ran here: the output reports "${noRunReason}". Exit 1 is the runner's, not a test's.`);
|
|
985
|
+
if (autoFoundConfig) {
|
|
986
|
+
warnings.push(`SKYRAMP_PLAYWRIGHT_CONFIG wraps ${path.relative(hostCwd, autoFoundConfig) || autoFoundConfig}, the nearest Playwright config above the test file, because the command named none. Its testDir decides which files the run collects — name the config the file belongs to if this is the wrong one.`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const result = {
|
|
990
|
+
testFile: params.testFile,
|
|
991
|
+
status: verdictForExit(run.exitCode, run.timedOut || !!run.spawnError, run.output),
|
|
992
|
+
executedAt,
|
|
993
|
+
duration: run.duration,
|
|
994
|
+
errors,
|
|
995
|
+
warnings,
|
|
996
|
+
output: run.output,
|
|
997
|
+
...(run.exitCode !== null ? { exitCode: run.exitCode } : {}),
|
|
998
|
+
...(videoPath ? { videoPath } : {}),
|
|
999
|
+
};
|
|
1000
|
+
const stateWarning = await writeExecutionToState(params, result, budget.reserved);
|
|
1001
|
+
if (stateWarning)
|
|
1002
|
+
warnings.push(stateWarning);
|
|
1003
|
+
await recordExecutionVideo(result, params.stateFile);
|
|
1004
|
+
const refreshText = snapshots.length > 0
|
|
1005
|
+
? await settleRefresh(params, snapshots, baselinesBefore)
|
|
1006
|
+
: "";
|
|
1007
|
+
const compose = (text) => [
|
|
1008
|
+
withVideoInfo(text, videoPath),
|
|
1009
|
+
refreshText,
|
|
1010
|
+
...warnings.map((w) => `Warning: ${w}`),
|
|
1011
|
+
]
|
|
1012
|
+
.filter(Boolean)
|
|
1013
|
+
.join("\n\n") + budget.noRunNote;
|
|
1014
|
+
if (result.status !== TestExecutionStatus.Pass) {
|
|
1015
|
+
return toolError(compose(buildExecutionFailureText(result, {
|
|
1016
|
+
unchangedRerun: budget.unchangedRerun,
|
|
1017
|
+
}) + budget.attemptLine));
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
content: [
|
|
1021
|
+
{
|
|
1022
|
+
type: "text",
|
|
1023
|
+
text: compose(`Test execution passed (exitCode=0, duration=${result.duration}ms).\n\n${result.output ?? ""}`),
|
|
1024
|
+
},
|
|
1025
|
+
],
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
320
1028
|
export function registerExecuteSkyrampTestTool(server) {
|
|
321
1029
|
server.registerTool(TOOL_NAME, {
|
|
322
|
-
description:
|
|
1030
|
+
description: "Run one test file and record the verdict from the exit code: 0 is Pass, 1 is Fail, any other exit or a timeout is Error. With `commandOverride` the server runs that command on this machine in `cwd`, with the server's environment. Without it the Skyramp executor container runs the test, which needs `workspacePath`. Either way the output is kept from its end, up to 200,000 characters.",
|
|
323
1031
|
annotations: {
|
|
324
1032
|
readOnlyHint: false,
|
|
325
|
-
destructiveHint:
|
|
1033
|
+
destructiveHint: true,
|
|
326
1034
|
idempotentHint: false,
|
|
327
1035
|
openWorldHint: true,
|
|
328
1036
|
},
|
|
329
|
-
inputSchema
|
|
330
|
-
workspacePath: z
|
|
331
|
-
.string()
|
|
332
|
-
.describe("The path to the workspace directory where the test file is located"),
|
|
333
|
-
language: z
|
|
334
|
-
.nativeEnum(ProgrammingLanguage)
|
|
335
|
-
.describe("Programming language of the test file to execute (e.g., python, javascript, typescript, java)"),
|
|
336
|
-
testType: z
|
|
337
|
-
.nativeEnum(TestType)
|
|
338
|
-
.describe("Type of the test to execute. Note: 'mock' is NOT a valid test type — mock files are deployed via their apply_mock() function, not executed as tests."),
|
|
339
|
-
testFile: z
|
|
340
|
-
.string()
|
|
341
|
-
.describe("Absolute path to the test file to execute."),
|
|
342
|
-
contractMode: z
|
|
343
|
-
.enum(CONTRACT_EXECUTION_MODES)
|
|
344
|
-
.optional()
|
|
345
|
-
.describe("Only applies when testType is 'contract'. Use 'provider' for provider contract tests that hit the real service under test and need SKYRAMP_TEST_BASE_URL. Use 'consumer' only for consumer contract tests with inline mocks that do not hit the real service. Defaults to provider behavior when omitted."),
|
|
346
|
-
token: z
|
|
347
|
-
.string()
|
|
348
|
-
.optional()
|
|
349
|
-
.describe("Explicit Skyramp authentication token for test execution. Omit this parameter to use SKYRAMP_TEST_TOKEN from the environment. An empty string is passed through as a literal token value, not a 'no auth' signal — use `unauthenticated: true` for tests that must run without credentials."),
|
|
350
|
-
unauthenticated: z
|
|
351
|
-
.boolean()
|
|
352
|
-
.optional()
|
|
353
|
-
.describe("Set true to force this test execution to carry NO auth token, even if SKYRAMP_TEST_TOKEN is set in the environment or a token was passed. Use for tests that must assert 401/403 unauthenticated behavior."),
|
|
354
|
-
playwrightSaveStoragePath: z
|
|
355
|
-
.string()
|
|
356
|
-
.optional()
|
|
357
|
-
.describe("Path to save Playwright session storage after test execution for authentication purposes. Can be a relative path to the workspace (e.g., 'auth-session.json') or an absolute path. The session will be saved after the test completes."),
|
|
358
|
-
stateFile: z
|
|
359
|
-
.string()
|
|
360
|
-
.optional()
|
|
361
|
-
.describe("Path to state file from skyramp_analyze_changes. Always pass when available — results are written back so skyramp_submit_report can override before/afterStatus with ground-truth pass/fail."),
|
|
362
|
-
phase: z
|
|
363
|
-
.enum(["before", "after"])
|
|
364
|
-
.default("after")
|
|
365
|
-
.describe("Execution phase for maintained tests: 'before' captures pre-edit baseline; 'after' (default) records post-edit result."),
|
|
366
|
-
rebaselineSnapshots: z
|
|
367
|
-
.array(rebaselineSnapshotNameSchema)
|
|
368
|
-
.optional()
|
|
369
|
-
.describe("UI tests only. toHaveScreenshot() baseline filenames (e.g. [\"page-001.png\"]) this run REPLACES instead of comparing against, because the PR intentionally changed how the captured page/element/region looks (SKYR-4298). " +
|
|
370
|
-
"Pass exactly the list skyramp_actions returned as rebaseline_snapshots for this spec — the tool checks it against the persisted UPDATE verdict in stateFile (so stateFile is required) and refuses names the verdict did not authorize, a spec with no such verdict, or a spec whose phase: 'before' run has not been recorded yet. Use the name as the test passes it (page-001.png), not the on-disk file (page-001-chromium-linux.png). " +
|
|
371
|
-
"The refreshed PNGs land beside the spec and are delivered with the Testbot PR as image diffs; the result names which baselines were refreshed and which were not. Never use this to silence a screenshot mismatch the diff does not explain."),
|
|
372
|
-
repository: z
|
|
373
|
-
.string()
|
|
374
|
-
.optional()
|
|
375
|
-
.describe("The owner/repo whose analysis section to write execution results into (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for a primary-repo test, or a related repo's owner/repo for that repo's section of the run-scoped stateFile. Omit only for a single-repo run."),
|
|
376
|
-
},
|
|
1037
|
+
inputSchema,
|
|
377
1038
|
_meta: {
|
|
378
1039
|
keywords: ["run test", "execute test"],
|
|
379
1040
|
},
|
|
380
1041
|
}, async (params, extra) => {
|
|
381
|
-
let errorResult;
|
|
382
|
-
// Helper to send progress notifications to the MCP client.
|
|
383
1042
|
const sendProgress = makeProgressReporter(extra);
|
|
384
|
-
//
|
|
385
|
-
await sendProgress(0, 100, "Starting execution...");
|
|
386
|
-
// Progress callback adapter for TestExecutionService
|
|
1043
|
+
// Progress callback adapter for TestExecutionService.
|
|
387
1044
|
const onExecutionProgress = async (progress) => {
|
|
388
1045
|
await sendProgress(progress.percent, 100, progress.message);
|
|
389
1046
|
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
1047
|
+
await sendProgress(0, 100, "Starting execution...");
|
|
1048
|
+
// `stateFile` may be omitted by the agent; the run's own state file (the
|
|
1049
|
+
// one skyramp_analyze_changes wrote for this run) is the fallback, so a
|
|
1050
|
+
// dropped argument cannot dodge the attempt cap or lose the result the
|
|
1051
|
+
// report reads. Only a call with no run at all executes uncounted. A blank
|
|
1052
|
+
// path is "omitted", not a file, so it takes the same fallback. Inside a
|
|
1053
|
+
// run, a named path must be THIS run's file under any spelling: another
|
|
1054
|
+
// run's (or a copied) state file would hand the file a fresh counter and
|
|
1055
|
+
// put the result where the report never looks, so it is refused before
|
|
1056
|
+
// anything is queued or written. The path is then canonicalized (real
|
|
1057
|
+
// path) so every later read and write names one file — a write through a
|
|
1058
|
+
// symlink would otherwise replace the link instead of the target.
|
|
1059
|
+
const requestedStateFile = params.stateFile?.trim();
|
|
1060
|
+
let stateFilePath;
|
|
1061
|
+
if (requestedStateFile) {
|
|
1062
|
+
const own = resolveOwnRunStatePath(requestedStateFile);
|
|
1063
|
+
if (own.refusal) {
|
|
1064
|
+
// Worded here, not with the shared refusal: that one ends "leave
|
|
1065
|
+
// stateFile out", and this tool's other messages and the prompt say
|
|
1066
|
+
// to pass it on every call. Both are true; say so in one sentence.
|
|
1067
|
+
return toolError(`Not executed: stateFile names ${requestedStateFile}, which is not this run's state file. ` +
|
|
1068
|
+
`Pass this run's stateFile (${currentRunStateFile()}) with repository — or leave stateFile out, ` +
|
|
1069
|
+
`and the run's own file is used. The attempt cap is enforced against that file either way.`);
|
|
402
1070
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1071
|
+
stateFilePath = own.path;
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
stateFilePath = currentRunStateFile();
|
|
1075
|
+
}
|
|
1076
|
+
if (stateFilePath)
|
|
1077
|
+
stateFilePath = canonicalStateFilePath(stateFilePath);
|
|
1078
|
+
// The "no run at all" branch: nothing below counts or records. Say so
|
|
1079
|
+
// where an operator will look six months later, and tell the agent the
|
|
1080
|
+
// result will not reach the report.
|
|
1081
|
+
if (!stateFilePath) {
|
|
1082
|
+
logger.warning(`${TOOL_NAME} has no run state: attempt cap not enforced, result not recorded`, {
|
|
1083
|
+
testFile: params.testFile,
|
|
1084
|
+
resolvedRunStatePath: resolveRunStatePath(),
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
// One execution at a time per run: everything below that touches the
|
|
1088
|
+
// state file — the cap read, the reservation, the retrofit staging, the
|
|
1089
|
+
// assertion proof, the result, the video record, the baseline verdict —
|
|
1090
|
+
// is a read-modify-write of one section, so two calls in the same run
|
|
1091
|
+
// for ANY two files could overwrite each other. Serializing the whole
|
|
1092
|
+
// handler on the state file closes every one of those windows at once;
|
|
1093
|
+
// reserve/persist also serialize on this key and re-enter it. The prompt
|
|
1094
|
+
// already forbids parallel execute calls; this keeps the state exact even
|
|
1095
|
+
// when that is ignored. The key is the file's real path, so two spellings
|
|
1096
|
+
// of one state file share one queue. A call with no run state serializes
|
|
1097
|
+
// per file (a symlink and its target share one queue via the canonical path).
|
|
1098
|
+
return runSerialized(stateFilePath
|
|
1099
|
+
? stateFileKey(stateFilePath)
|
|
1100
|
+
: canonicalTestPath(params.testFile), async () => {
|
|
1101
|
+
let errorResult;
|
|
1102
|
+
try {
|
|
1103
|
+
if (!path.isAbsolute(params.testFile)) {
|
|
1104
|
+
errorResult = toolError(`testFile must be an absolute path, got: ${params.testFile}`);
|
|
407
1105
|
return errorResult;
|
|
408
1106
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1107
|
+
// A missing test file is an argument error: refuse it before any side effect
|
|
1108
|
+
// (retrofit staging, the assertion proof record, the attempt reservation) has
|
|
1109
|
+
// recorded an execution that never ran.
|
|
1110
|
+
if (!fs.existsSync(params.testFile)) {
|
|
1111
|
+
errorResult = toolError(`Test file does not exist: ${params.testFile}`);
|
|
413
1112
|
return errorResult;
|
|
414
1113
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const entry = stateData?.existingTests?.find((t) => t.testFile === params.testFile);
|
|
428
|
-
if (entry?.source === TestSource.External) {
|
|
429
|
-
logger.info(`Skipping execution of external test ${params.testFile} — native suites are not run by ${TOOL_NAME}`);
|
|
430
|
-
return {
|
|
431
|
-
content: [
|
|
432
|
-
{
|
|
433
|
-
type: "text",
|
|
434
|
-
text: `Skipped execution: ${params.testFile} is marked external in the state file. ${TOOL_NAME} runs Skyramp-generated tests only; external (native) suites are not executed here.`,
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
};
|
|
1114
|
+
// Validate the inputs of whichever mode this call is in, before anything
|
|
1115
|
+
// below writes state: a call that cannot run must not record that it did.
|
|
1116
|
+
const command = params.commandOverride;
|
|
1117
|
+
const cwd = params.cwd;
|
|
1118
|
+
if (command === undefined) {
|
|
1119
|
+
if (params.workspacePath === undefined) {
|
|
1120
|
+
errorResult = toolError("workspacePath is required when commandOverride is omitted: the executor resolves the service and its base URL from the workspace. Pass commandOverride to run the test on this host instead.");
|
|
1121
|
+
return errorResult;
|
|
1122
|
+
}
|
|
1123
|
+
if (!path.isAbsolute(params.workspacePath)) {
|
|
1124
|
+
errorResult = toolError(`workspacePath must be an absolute path, got: ${params.workspacePath}`);
|
|
1125
|
+
return errorResult;
|
|
438
1126
|
}
|
|
439
1127
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
await stageAndRecordRetrofits(params.testFile, undefined, params.stateFile);
|
|
449
|
-
// SKYR-4115 backstop: enhance_assertions carries the same check, but nothing
|
|
450
|
-
// guarantees the agent calls it, and execution is the last step that still
|
|
451
|
-
// precedes reporting. See pendingReuseVerification for why enforcing the verify
|
|
452
|
-
// CALL needs no retry budget.
|
|
453
|
-
//
|
|
454
|
-
// Pass params.stateFile through: without it the state path resolves from the
|
|
455
|
-
// CI/Testbot anchor alone and the check returns early — failing OPEN — for a
|
|
456
|
-
// caller that supplies a valid stateFile outside CI. The external-test guard
|
|
457
|
-
// below already reads params.stateFile, so this one honours the same input.
|
|
458
|
-
const owedReuseVerification = await pendingReuseDebt(params.testFile, params.stateFile, params.testType);
|
|
459
|
-
if (owedReuseVerification) {
|
|
460
|
-
errorResult = toolError(owedReuseVerification);
|
|
461
|
-
return errorResult;
|
|
462
|
-
}
|
|
463
|
-
// Deterministic assertion-enhancement check: the server verifies the file
|
|
464
|
-
// itself here (never relying on the agent to call `verify: true` — prose
|
|
465
|
-
// can be ignored, this cannot). Insufficient assertions return feedback
|
|
466
|
-
// instead of executing; a fixed file passes on the next execute call.
|
|
467
|
-
// Sits BELOW the external-test guard: an external test is skipped, not
|
|
468
|
-
// executed, so deferring it for assertion work would demand fixes to a
|
|
469
|
-
// file this tool will never run.
|
|
470
|
-
const assertionFeedback = await assertionFeedbackForExecution(params.testFile, params.stateFile);
|
|
471
|
-
if (assertionFeedback) {
|
|
472
|
-
errorResult = toolError(assertionFeedback);
|
|
473
|
-
return errorResult;
|
|
474
|
-
}
|
|
475
|
-
// Proof-of-work substrate (SKYR-4262 follow-up): count this execution
|
|
476
|
-
// server-side so the report can cross-check generated vs executed —
|
|
477
|
-
// the narrative is LLM-authored, this number is not. Best-effort.
|
|
478
|
-
await recordAssertionExecution(params.testFile, params.testType, params.stateFile);
|
|
479
|
-
// Send initial progress
|
|
480
|
-
await sendProgress(5, 100, "Starting test execution...");
|
|
481
|
-
// Cross-repo run (SKYR-3819): tests delivered under the run's testsRepoDir
|
|
482
|
-
// (set by skyramp_analyze_changes, run-scoped) live outside workspacePath.
|
|
483
|
-
// Thread it through so the executor mounts the test repo and service
|
|
484
|
-
// matching still resolves the SUT baseUrl/dockerNetwork.
|
|
485
|
-
const testRepoPath = getTestsRepoDir();
|
|
486
|
-
// Resolve workspace config for base URL injection and Docker network
|
|
487
|
-
// attachment. A compose dockerNetwork is not host networking: on macOS
|
|
488
|
-
// the executor still needs localhost rewritten to host.docker.internal.
|
|
489
|
-
if (params.workspacePath) {
|
|
490
|
-
const workspaceConfig = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language, testRepoPath);
|
|
491
|
-
const { baseUrl, candidates } = workspaceConfig;
|
|
492
|
-
dockerNetwork = workspaceConfig.dockerNetwork;
|
|
493
|
-
const shouldInjectBaseUrl = shouldInjectSkyrampBaseUrl(params.testType, params.contractMode);
|
|
494
|
-
if (shouldInjectBaseUrl && !process.env.SKYRAMP_TEST_BASE_URL) {
|
|
495
|
-
if (baseUrl) {
|
|
496
|
-
process.env.SKYRAMP_TEST_BASE_URL = baseUrl;
|
|
497
|
-
didSetSkyrampBaseUrl = true;
|
|
1128
|
+
else {
|
|
1129
|
+
if (cwd === undefined) {
|
|
1130
|
+
errorResult = toolError("cwd is required with commandOverride.");
|
|
1131
|
+
return errorResult;
|
|
1132
|
+
}
|
|
1133
|
+
if (!path.isAbsolute(cwd)) {
|
|
1134
|
+
errorResult = toolError(`cwd must be an absolute path, got: ${cwd}`);
|
|
1135
|
+
return errorResult;
|
|
498
1136
|
}
|
|
499
|
-
|
|
500
|
-
errorResult = toolError(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
].join("\n"));
|
|
1137
|
+
if (!fs.statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
1138
|
+
errorResult = toolError(`cwd ${cwd} is not an existing directory. The command was not run.`);
|
|
1139
|
+
return errorResult;
|
|
1140
|
+
}
|
|
1141
|
+
if (!commandNamesTestFile(command, params.testFile)) {
|
|
1142
|
+
errorResult = toolError(`commandOverride does not contain ${testFileNames(params.testFile).join(" or ")}, the name of testFile. The command was not run.`);
|
|
506
1143
|
return errorResult;
|
|
507
1144
|
}
|
|
508
1145
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
// Execute test with progress callback - reports Docker cache/pull status.
|
|
518
|
-
// Retry once on transient connection errors (EOF, connection refused) —
|
|
519
|
-
// these indicate the SUT isn't fully ready yet, not a test failure.
|
|
520
|
-
// Retrying inside the tool call saves agent turns vs failing and
|
|
521
|
-
// requiring the agent to re-invoke.
|
|
522
|
-
const execOptions = {
|
|
523
|
-
testFile: params.testFile,
|
|
524
|
-
workspacePath: params.workspacePath,
|
|
525
|
-
testRepoPath,
|
|
526
|
-
language: params.language,
|
|
527
|
-
testType: params.testType,
|
|
528
|
-
token: effectiveToken,
|
|
529
|
-
playwrightSaveStoragePath: params.playwrightSaveStoragePath,
|
|
530
|
-
dockerNetwork,
|
|
531
|
-
useHostNetwork: false,
|
|
532
|
-
...(rebaselineSnapshots.length > 0 ? { rebaselineSnapshots } : {}),
|
|
533
|
-
};
|
|
534
|
-
// Identity of the requested baselines before the run, to report afterwards
|
|
535
|
-
// which ones SmartPlaywright actually rewrote (SKYR-4298).
|
|
536
|
-
const baselinesBefore = rebaselineSnapshots.length > 0
|
|
537
|
-
? readBaselineState(params.testFile, rebaselineSnapshots)
|
|
538
|
-
: {};
|
|
539
|
-
let result;
|
|
540
|
-
try {
|
|
541
|
-
result = await executionService.executeTest(execOptions, onExecutionProgress);
|
|
542
|
-
}
|
|
543
|
-
catch (firstErr) {
|
|
544
|
-
const errMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
545
|
-
if (/\bEOF\b|connection refused|ECONNREFUSED|ECONNRESET/i.test(errMsg)) {
|
|
546
|
-
logger.info("Test execution hit transient connection error, retrying after 10s...", { error: errMsg });
|
|
547
|
-
await sendProgress(50, 100, "SUT connection error — retrying in 10s...");
|
|
548
|
-
await new Promise((r) => setTimeout(r, 10_000));
|
|
549
|
-
result = await executionService.executeTest(execOptions, onExecutionProgress);
|
|
1146
|
+
// Hashed once, before any state write: recorded with the reservation
|
|
1147
|
+
// so the NEXT run can tell an unchanged re-run from a fixed file.
|
|
1148
|
+
const fileHash = sha256Of(fs.readFileSync(params.testFile, "utf8"));
|
|
1149
|
+
const rebaseline = resolveRebaselineSnapshots(params.rebaselineSnapshots, params.phase);
|
|
1150
|
+
if (rebaseline.error) {
|
|
1151
|
+
errorResult = toolError(rebaseline.error);
|
|
1152
|
+
return errorResult;
|
|
550
1153
|
}
|
|
551
|
-
|
|
552
|
-
|
|
1154
|
+
const snapshots = rebaseline.snapshots;
|
|
1155
|
+
if (snapshots.length > 0) {
|
|
1156
|
+
if (!stateFilePath) {
|
|
1157
|
+
errorResult = toolError("rebaselineSnapshots requires stateFile: the refresh is authorized against the UPDATE verdict skyramp_actions persisted there.");
|
|
1158
|
+
return errorResult;
|
|
1159
|
+
}
|
|
1160
|
+
const authState = await StateManager.fromStatePath(stateFilePath).readRepoData(params.repository);
|
|
1161
|
+
const auth = authorizeRebaseline(authState, params.testFile, snapshots);
|
|
1162
|
+
if (auth.error) {
|
|
1163
|
+
errorResult = toolError(auth.error);
|
|
1164
|
+
return errorResult;
|
|
1165
|
+
}
|
|
553
1166
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1167
|
+
// `stateFile` says whether there is any run state to count against and write
|
|
1168
|
+
// into; `repository` names the section inside it. A standalone execution —
|
|
1169
|
+
// the local-dev workflow, an IDE call, no run — names its repository but
|
|
1170
|
+
// writes nothing, and the tool says so in its answer.
|
|
1171
|
+
const stateSection = stateFilePath
|
|
1172
|
+
? { file: stateFilePath, repo: params.repository }
|
|
1173
|
+
: undefined;
|
|
1174
|
+
const noRunNote = stateSection
|
|
1175
|
+
? ""
|
|
1176
|
+
: `\n\nNote: no run state file was found (none passed, and no active run), so this execution was not counted against an attempt cap and its result is not recorded for the report.`;
|
|
1177
|
+
// Fix-and-rerun attempt cap (SKYR-4460). With a stateFile the cap is the one
|
|
1178
|
+
// pinned in the run's state (the first call pins the prompt's value), and the
|
|
1179
|
+
// check fails CLOSED: this is the enforcement boundary, so a state that cannot
|
|
1180
|
+
// be read refuses the run instead of counting as zero prior attempts. A call
|
|
1181
|
+
// without any run state resolves from the environment and the default and
|
|
1182
|
+
// enforces nothing — there is no run to count against.
|
|
1183
|
+
let maxFixAttempts = getMaxFixAttempts();
|
|
1184
|
+
// Whether this run re-executes the same file contents as the last
|
|
1185
|
+
// one (SKYR-4460): the single unchanged re-run a file gets is then
|
|
1186
|
+
// spent, and the failure texts say so instead of offering another.
|
|
1187
|
+
let unchangedRerun = false;
|
|
1188
|
+
let priorAfterRuns = 0;
|
|
1189
|
+
if (stateSection) {
|
|
1190
|
+
let record;
|
|
1191
|
+
try {
|
|
1192
|
+
const stateData = await readRepoSectionOrThrow(stateSection.file, stateSection.repo);
|
|
1193
|
+
// The pin is run-wide and lives at the ROOT, whichever section this
|
|
1194
|
+
// file's record is in. A pinned value is re-validated: a hand-edited or
|
|
1195
|
+
// half-written state must not turn into "cap 0, nothing ever runs" or
|
|
1196
|
+
// "no cap".
|
|
1197
|
+
const pinned = await readPinnedMaxFixAttempts(stateSection.file);
|
|
1198
|
+
const current = getMaxFixAttempts();
|
|
1199
|
+
maxFixAttempts = pinned ?? current;
|
|
1200
|
+
if (pinned !== undefined && pinned !== current) {
|
|
1201
|
+
logger.warning("Enforcing the attempt cap pinned in the run's state, not the current prompt's", { pinned, current, stateFile: stateSection.file });
|
|
578
1202
|
}
|
|
1203
|
+
record =
|
|
1204
|
+
stateData.testExecutions?.[canonicalTestPath(params.testFile)];
|
|
1205
|
+
}
|
|
1206
|
+
catch (err) {
|
|
1207
|
+
errorResult = toolError(`Not executed: the attempt-cap check could not use stateFile ${stateSection.file} — ${err.message}. ` +
|
|
1208
|
+
`Fix the stateFile or repository argument and call again; do not drop stateFile.`);
|
|
1209
|
+
return errorResult;
|
|
1210
|
+
}
|
|
1211
|
+
priorAfterRuns = record?.afterRuns ?? 0;
|
|
1212
|
+
unchangedRerun =
|
|
1213
|
+
params.phase === "after" &&
|
|
1214
|
+
record?.phase === "after" &&
|
|
1215
|
+
record.fileHash !== undefined &&
|
|
1216
|
+
record.fileHash === fileHash;
|
|
1217
|
+
const refusal = fixAttemptRefusal(record, params.phase, maxFixAttempts, params.testFile);
|
|
1218
|
+
if (refusal) {
|
|
1219
|
+
logger.info(`Refusing execution of ${params.testFile}: attempt cap ${maxFixAttempts} reached`);
|
|
1220
|
+
errorResult = toolError(refusal);
|
|
1221
|
+
return errorResult;
|
|
579
1222
|
}
|
|
580
1223
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1224
|
+
// SKYR-4220: a utils file left unstaged ships a test importing a module the PR lacks.
|
|
1225
|
+
await stageAndRecordRetrofits(params.testFile, undefined, stateFilePath);
|
|
1226
|
+
const owedReuse = await pendingReuseDebt(params.testFile, stateFilePath, params.testType);
|
|
1227
|
+
if (owedReuse) {
|
|
1228
|
+
errorResult = toolError(owedReuse);
|
|
1229
|
+
return errorResult;
|
|
584
1230
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
// directories the report references, so an unrecorded video is never seen.
|
|
590
|
-
await recordExecutionVideo(result, params.stateFile);
|
|
591
|
-
// Which requested baselines were actually rewritten (SKYR-4298). Reported on
|
|
592
|
-
// pass and fail alike; a refreshed PNG is a deliverable like a generated
|
|
593
|
-
// spec, so stage the spec's snapshot directory whenever one was rewritten —
|
|
594
|
-
// even on a failing run, since the report gate checks the PNG, not the
|
|
595
|
-
// status — so the eval harness commit and the artifact collector see it
|
|
596
|
-
// (production delivery adds the whole test directory anyway). No-op outside
|
|
597
|
-
// a testbot run, like every other stageGeneratedPaths call; never fails the
|
|
598
|
-
// execution.
|
|
599
|
-
let refreshOutcomeText = "";
|
|
600
|
-
if (rebaselineSnapshots.length > 0) {
|
|
601
|
-
const after = readBaselineState(params.testFile, rebaselineSnapshots);
|
|
602
|
-
const outcome = diffBaselineState(baselinesBefore, after);
|
|
603
|
-
refreshOutcomeText = describeRefreshOutcome(outcome);
|
|
604
|
-
// Stage the rewritten files themselves, never the directory: `git add` on
|
|
605
|
-
// the directory would ship anything else sitting there under one
|
|
606
|
-
// authorized baseline's authority.
|
|
607
|
-
for (const name of outcome.refreshed) {
|
|
608
|
-
for (const file of outcome.refreshedFiles[name] ?? []) {
|
|
609
|
-
try {
|
|
610
|
-
await stageGeneratedPaths(path.join(snapshotDirFor(params.testFile), file));
|
|
611
|
-
}
|
|
612
|
-
catch (err) {
|
|
613
|
-
logger.warning(`Could not stage refreshed visual baseline ${file} for ${params.testFile}: ${err.message}`);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
1231
|
+
const assertionFeedback = await assertionFeedbackForExecution(params.testFile, stateFilePath);
|
|
1232
|
+
if (assertionFeedback) {
|
|
1233
|
+
errorResult = toolError(assertionFeedback);
|
|
1234
|
+
return errorResult;
|
|
616
1235
|
}
|
|
617
|
-
|
|
1236
|
+
await recordAssertionExecution(params.testFile, params.testType, stateFilePath);
|
|
1237
|
+
// Reserve the attempt before the run (SKYR-4460). It sits ABOVE the mode
|
|
1238
|
+
// dispatch on purpose: the host path spawns the command itself and never
|
|
1239
|
+
// reaches TestExecutionService, so a reservation taken around the executor
|
|
1240
|
+
// call would count nothing on the path this tool now prefers. The counters
|
|
1241
|
+
// advance now and the cap is pinned, so a state write that fails after the
|
|
1242
|
+
// run cannot hand this file a free attempt. Fails closed, like the check above.
|
|
1243
|
+
let reserved = false;
|
|
1244
|
+
if (stateSection) {
|
|
618
1245
|
try {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
await stateManager.writeRepoData({ ...stateData, maintenanceVerdicts: reconciled.verdicts }, { repo: params.repository });
|
|
624
|
-
if (reconciled.note)
|
|
625
|
-
refreshOutcomeText += ` ${reconciled.note}`;
|
|
626
|
-
}
|
|
1246
|
+
const reservation = await reserveTestExecutionAttempt(stateSection.file, stateSection.repo, params.testType, params.phase, params.testFile, maxFixAttempts, fileHash);
|
|
1247
|
+
priorAfterRuns = reservation.priorAfterRuns;
|
|
1248
|
+
maxFixAttempts = reservation.cap;
|
|
1249
|
+
reserved = true;
|
|
627
1250
|
}
|
|
628
1251
|
catch (err) {
|
|
629
|
-
|
|
1252
|
+
errorResult = toolError(`Not executed: could not reserve the execution attempt in stateFile ${stateSection.file} (${err.message}).`);
|
|
1253
|
+
return errorResult;
|
|
630
1254
|
}
|
|
631
1255
|
}
|
|
1256
|
+
const budget = {
|
|
1257
|
+
// The budget line only means something when the run counts attempts, i.e.
|
|
1258
|
+
// when a stateFile records them and this is a final-phase run.
|
|
1259
|
+
attemptLine: stateSection && params.phase === "after"
|
|
1260
|
+
? `\n\n${describeAttempt(priorAfterRuns, maxFixAttempts, { unchangedRerun })}`
|
|
1261
|
+
: "",
|
|
1262
|
+
reserved,
|
|
1263
|
+
noRunNote,
|
|
1264
|
+
unchangedRerun,
|
|
1265
|
+
};
|
|
1266
|
+
await sendProgress(5, 100, "Starting test execution...");
|
|
1267
|
+
// Both modes owe the checks above. Only now does the path diverge.
|
|
1268
|
+
// Both take the RESOLVED state path, not the argument: everything they
|
|
1269
|
+
// hand it to — the result write, the video record, the verdict
|
|
1270
|
+
// reconciliation — must name the same file every check above read.
|
|
1271
|
+
const runParams = {
|
|
1272
|
+
...params,
|
|
1273
|
+
stateFile: stateFilePath,
|
|
1274
|
+
};
|
|
1275
|
+
try {
|
|
1276
|
+
return command === undefined
|
|
1277
|
+
? await runInExecutor(runParams, snapshots, budget, onExecutionProgress, sendProgress)
|
|
1278
|
+
: await runOnHost(runParams, command, cwd, snapshots, budget);
|
|
1279
|
+
}
|
|
1280
|
+
catch (err) {
|
|
1281
|
+
return accountForThrow(runParams, err, budget);
|
|
1282
|
+
}
|
|
632
1283
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// Only report final status if not already at 100%
|
|
636
|
-
if (result.status !== TestExecutionStatus.Pass) {
|
|
637
|
-
errorResult = toolError(withRefreshOutcome(withVideoInfo(buildExecutionFailureText(result), result.videoPath)));
|
|
1284
|
+
catch (err) {
|
|
1285
|
+
errorResult = toolError(`Test execution failed: ${err.message}`);
|
|
638
1286
|
return errorResult;
|
|
639
1287
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
catch (err) {
|
|
651
|
-
errorResult = toolError(`Test execution failed: ${err.message}`);
|
|
652
|
-
return errorResult;
|
|
653
|
-
}
|
|
654
|
-
finally {
|
|
655
|
-
if (didSetSkyrampBaseUrl) {
|
|
656
|
-
if (previousBaseUrl === undefined) {
|
|
657
|
-
delete process.env.SKYRAMP_TEST_BASE_URL;
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
process.env.SKYRAMP_TEST_BASE_URL = previousBaseUrl;
|
|
661
|
-
}
|
|
1288
|
+
finally {
|
|
1289
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
1290
|
+
testFile: params.testFile,
|
|
1291
|
+
language: params.language,
|
|
1292
|
+
testType: params.testType,
|
|
1293
|
+
}).catch((err) => {
|
|
1294
|
+
logger.warning("Analytics event failed", { error: String(err) });
|
|
1295
|
+
});
|
|
662
1296
|
}
|
|
663
|
-
|
|
664
|
-
testFile: params.testFile,
|
|
665
|
-
workspacePath: params.workspacePath,
|
|
666
|
-
language: params.language,
|
|
667
|
-
testType: params.testType,
|
|
668
|
-
}).catch((err) => {
|
|
669
|
-
logger.warning("Analytics event failed", { error: String(err) });
|
|
670
|
-
});
|
|
671
|
-
}
|
|
1297
|
+
});
|
|
672
1298
|
});
|
|
673
1299
|
}
|