@skyramp/mcp 0.3.8 → 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { TestExecutionResult } from "../types/TestExecution.js";
|
|
3
3
|
import { TestType } from "../types/TestTypes.js";
|
|
4
|
+
import { MaintenanceActionCore, TestAnalysisResult } from "../types/TestAnalysis.js";
|
|
4
5
|
export declare const CONTRACT_EXECUTION_MODES: readonly ["provider", "consumer"];
|
|
5
6
|
export type ContractExecutionMode = (typeof CONTRACT_EXECUTION_MODES)[number];
|
|
6
7
|
/**
|
|
@@ -20,6 +21,16 @@ export declare function shouldInjectSkyrampBaseUrl(testType: TestType, contractM
|
|
|
20
21
|
* its own attachment line.
|
|
21
22
|
*/
|
|
22
23
|
export declare function withVideoInfo(output: string, videoPath?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize the `rebaselineSnapshots` request (SKYR-4298): dedupe and drop blanks,
|
|
26
|
+
* and refuse it on the pre-edit run. A refresh there would overwrite the very
|
|
27
|
+
* evidence that the baseline is stale, and `beforeStatus` would read Pass for a
|
|
28
|
+
* test that failed against the committed image.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveRebaselineSnapshots(requested: string[] | undefined, phase: "before" | "after" | undefined): {
|
|
31
|
+
snapshots: string[];
|
|
32
|
+
error?: string;
|
|
33
|
+
};
|
|
23
34
|
/**
|
|
24
35
|
* The failure text the agent receives. Everything it needs has to be in here:
|
|
25
36
|
* only a tool's return value reaches the transcript, and Claude Code does not
|
|
@@ -37,5 +48,74 @@ export declare function withVideoInfo(output: string, videoPath?: string): strin
|
|
|
37
48
|
* agent at authentication on a run whose output was empty, and then it keyed off
|
|
38
49
|
* the bare number, which pointed it there on a test count or a line number.
|
|
39
50
|
*/
|
|
51
|
+
/** The on-disk files a requested baseline name resolves to; empty when none matches. */
|
|
52
|
+
export type BaselineFileState = Array<{
|
|
53
|
+
file: string;
|
|
54
|
+
size: number;
|
|
55
|
+
mtimeMs: number;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot of the requested baselines under `<spec>-snapshots/` (SKYR-4298): for
|
|
59
|
+
* each requested name, every PNG whose name matches the stem (a spec may hold both
|
|
60
|
+
* `<stem>-linux.png` and `<stem>-chromium-linux.png`; latching onto one of them would
|
|
61
|
+
* misreport the other) with its size and mtime. Taken before and after the run so
|
|
62
|
+
* the tool can tell the agent which baselines were actually rewritten — SmartPlaywright
|
|
63
|
+
* is the only party that knows the exact filename, and an executor image that lacks
|
|
64
|
+
* SKYRAMP_UPDATE_SNAPSHOTS (or a name matching no toHaveScreenshot call) leaves
|
|
65
|
+
* every file untouched.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readBaselineState(specFile: string, requested: string[]): Record<string, BaselineFileState>;
|
|
68
|
+
/**
|
|
69
|
+
* Which requested baselines changed on disk between two readBaselineState calls, and
|
|
70
|
+
* which files carried the change (the ones to stage).
|
|
71
|
+
*/
|
|
72
|
+
export declare function diffBaselineState(before: Record<string, BaselineFileState>, after: Record<string, BaselineFileState>): {
|
|
73
|
+
refreshed: string[];
|
|
74
|
+
notRefreshed: string[];
|
|
75
|
+
refreshedFiles: Record<string, string[]>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Authorize a refresh against the persisted maintenance state (SKYR-4298). The
|
|
79
|
+
* parameter description tells the agent to pass exactly what skyramp_actions
|
|
80
|
+
* returned, but prose cannot tell an authorized list from an invented one, so the
|
|
81
|
+
* tool checks: an UPDATE verdict for this spec must list every requested name, and
|
|
82
|
+
* the spec's phase:"before" run must already be recorded — a refresh before that
|
|
83
|
+
* run would overwrite the very evidence that the baseline is stale, whatever
|
|
84
|
+
* `phase` says or omits. A newly generated test has no verdict and no committed
|
|
85
|
+
* baseline, so it can never be refreshed here.
|
|
86
|
+
*/
|
|
87
|
+
export declare function authorizeRebaseline(stateData: {
|
|
88
|
+
existingTests?: TestAnalysisResult[];
|
|
89
|
+
maintenanceVerdicts?: MaintenanceActionCore[];
|
|
90
|
+
} | null | undefined, testFile: string, requested: string[]): {
|
|
91
|
+
error?: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Reconcile the persisted verdict with what the executor actually did (SKYR-4298).
|
|
95
|
+
* An executor image whose @skyramp/skyramp predates SKYRAMP_UPDATE_SNAPSHOTS rewrites
|
|
96
|
+
* nothing; left alone, the verdict would still promise a refresh, the report gate
|
|
97
|
+
* would refuse the report, and nothing in the prompt makes the agent's way out
|
|
98
|
+
* deterministic. So: names that were not refreshed are dropped from the verdict; a
|
|
99
|
+
* rebaseline-only UPDATE with nothing left becomes VERIFY (the test stays red, the
|
|
100
|
+
* rationale says why), and an UPDATE that also carried edits keeps UPDATE and is
|
|
101
|
+
* held to its edit. The report then reflects what happened, not what was asked.
|
|
102
|
+
*/
|
|
103
|
+
export declare function applyRefreshOutcomeToVerdicts(verdicts: MaintenanceActionCore[], testFile: string, outcome: {
|
|
104
|
+
refreshed: string[];
|
|
105
|
+
notRefreshed: string[];
|
|
106
|
+
}, executorImage: string): {
|
|
107
|
+
verdicts: MaintenanceActionCore[];
|
|
108
|
+
note?: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The refresh outcome line appended to the execution result, on pass and on fail
|
|
112
|
+
* alike. Without it a stale-baseline run that could not refresh reads as a bare
|
|
113
|
+
* "Screenshot comparison failed", and the report gate then tells the agent to run
|
|
114
|
+
* with rebaselineSnapshots set — which it already did.
|
|
115
|
+
*/
|
|
116
|
+
export declare function describeRefreshOutcome(outcome: {
|
|
117
|
+
refreshed: string[];
|
|
118
|
+
notRefreshed: string[];
|
|
119
|
+
}): string;
|
|
40
120
|
export declare function buildExecutionFailureText(result: TestExecutionResult): string;
|
|
41
121
|
export declare function registerExecuteSkyrampTestTool(server: McpServer): void;
|
|
@@ -11,10 +11,14 @@ import { TestExecutionStatus, } from "../types/TestExecution.js";
|
|
|
11
11
|
import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
|
|
12
12
|
import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
|
|
13
13
|
import { StateManager, getTestsRepoDir, } from "../utils/AnalysisStateManager.js";
|
|
14
|
-
import { TestSource } from "../types/TestAnalysis.js";
|
|
14
|
+
import { DriftAction, TestSource } from "../types/TestAnalysis.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
16
|
import { toolError } from "../utils/utils.js";
|
|
17
17
|
import { recordExecutionVideo } from "./execution-video-state.js";
|
|
18
|
+
import { stageGeneratedPaths } from "../utils/gitStaging.js";
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import { baselineFileMatchesStem, baselineStem, rebaselineSnapshotNameSchema, snapshotDirFor, } from "../utils/rebaselineSnapshots.js";
|
|
21
|
+
import { EXECUTOR_DOCKER_IMAGE } from "../utils/versions.js";
|
|
18
22
|
const TOOL_NAME = "skyramp_execute_test";
|
|
19
23
|
export const CONTRACT_EXECUTION_MODES = ["provider", "consumer"];
|
|
20
24
|
/**
|
|
@@ -97,22 +101,172 @@ const HTTP_401_SHAPES = [
|
|
|
97
101
|
/\bwhere\s+401\s*=/i,
|
|
98
102
|
];
|
|
99
103
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* the
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
* Normalize the `rebaselineSnapshots` request (SKYR-4298): dedupe and drop blanks,
|
|
105
|
+
* and refuse it on the pre-edit run. A refresh there would overwrite the very
|
|
106
|
+
* evidence that the baseline is stale, and `beforeStatus` would read Pass for a
|
|
107
|
+
* test that failed against the committed image.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveRebaselineSnapshots(requested, phase) {
|
|
110
|
+
const snapshots = [
|
|
111
|
+
...new Set((requested ?? []).map((s) => s.trim()).filter(Boolean)),
|
|
112
|
+
];
|
|
113
|
+
if (snapshots.length > 0 && phase === "before") {
|
|
114
|
+
return {
|
|
115
|
+
snapshots: [],
|
|
116
|
+
error: `rebaselineSnapshots cannot be combined with phase: "before". Run the pre-edit baseline without it to record the mismatch, then pass rebaselineSnapshots on the final run only.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { snapshots };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Snapshot of the requested baselines under `<spec>-snapshots/` (SKYR-4298): for
|
|
123
|
+
* each requested name, every PNG whose name matches the stem (a spec may hold both
|
|
124
|
+
* `<stem>-linux.png` and `<stem>-chromium-linux.png`; latching onto one of them would
|
|
125
|
+
* misreport the other) with its size and mtime. Taken before and after the run so
|
|
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.
|
|
115
130
|
*/
|
|
131
|
+
export function readBaselineState(specFile, requested) {
|
|
132
|
+
const dir = snapshotDirFor(specFile);
|
|
133
|
+
let entries = [];
|
|
134
|
+
try {
|
|
135
|
+
entries = fs.readdirSync(dir).sort();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
entries = [];
|
|
139
|
+
}
|
|
140
|
+
const state = {};
|
|
141
|
+
for (const name of requested) {
|
|
142
|
+
const stem = baselineStem(name);
|
|
143
|
+
const files = [];
|
|
144
|
+
for (const file of entries.filter((e) => baselineFileMatchesStem(e, stem))) {
|
|
145
|
+
try {
|
|
146
|
+
const st = fs.statSync(path.join(dir, file));
|
|
147
|
+
files.push({ file, size: st.size, mtimeMs: st.mtimeMs });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// vanished between readdir and stat — treat as absent
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
state[name] = files;
|
|
154
|
+
}
|
|
155
|
+
return state;
|
|
156
|
+
}
|
|
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
|
+
export function diffBaselineState(before, after) {
|
|
162
|
+
const refreshed = [];
|
|
163
|
+
const notRefreshed = [];
|
|
164
|
+
const refreshedFiles = {};
|
|
165
|
+
for (const name of Object.keys(after)) {
|
|
166
|
+
const prev = new Map((before[name] ?? []).map((f) => [f.file, f]));
|
|
167
|
+
const changed = after[name]
|
|
168
|
+
.filter((a) => {
|
|
169
|
+
const b = prev.get(a.file);
|
|
170
|
+
return !b || a.size !== b.size || a.mtimeMs !== b.mtimeMs;
|
|
171
|
+
})
|
|
172
|
+
.map((a) => a.file);
|
|
173
|
+
if (changed.length > 0) {
|
|
174
|
+
refreshed.push(name);
|
|
175
|
+
refreshedFiles[name] = changed;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
notRefreshed.push(name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { refreshed, notRefreshed, refreshedFiles };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Authorize a refresh against the persisted maintenance state (SKYR-4298). The
|
|
185
|
+
* parameter description tells the agent to pass exactly what skyramp_actions
|
|
186
|
+
* returned, but prose cannot tell an authorized list from an invented one, so the
|
|
187
|
+
* tool checks: an UPDATE verdict for this spec must list every requested name, and
|
|
188
|
+
* the spec's phase:"before" run must already be recorded — a refresh before that
|
|
189
|
+
* run would overwrite the very evidence that the baseline is stale, whatever
|
|
190
|
+
* `phase` says or omits. A newly generated test has no verdict and no committed
|
|
191
|
+
* baseline, so it can never be refreshed here.
|
|
192
|
+
*/
|
|
193
|
+
export function authorizeRebaseline(stateData, testFile, requested) {
|
|
194
|
+
if (requested.length === 0)
|
|
195
|
+
return {};
|
|
196
|
+
const verdict = (stateData?.maintenanceVerdicts ?? []).find((v) => v.testFilePath === testFile && v.action === DriftAction.Update);
|
|
197
|
+
if (!verdict) {
|
|
198
|
+
return {
|
|
199
|
+
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.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const authorized = new Set(verdict.rebaselineSnapshots ?? []);
|
|
203
|
+
const unauthorized = requested.filter((n) => !authorized.has(n));
|
|
204
|
+
if (unauthorized.length > 0) {
|
|
205
|
+
return {
|
|
206
|
+
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
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const entry = (stateData?.existingTests ?? []).find((t) => t.testFile === testFile);
|
|
210
|
+
if (!entry?.executionBefore) {
|
|
211
|
+
return {
|
|
212
|
+
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
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Reconcile the persisted verdict with what the executor actually did (SKYR-4298).
|
|
219
|
+
* An executor image whose @skyramp/skyramp predates SKYRAMP_UPDATE_SNAPSHOTS rewrites
|
|
220
|
+
* nothing; left alone, the verdict would still promise a refresh, the report gate
|
|
221
|
+
* would refuse the report, and nothing in the prompt makes the agent's way out
|
|
222
|
+
* deterministic. So: names that were not refreshed are dropped from the verdict; a
|
|
223
|
+
* rebaseline-only UPDATE with nothing left becomes VERIFY (the test stays red, the
|
|
224
|
+
* rationale says why), and an UPDATE that also carried edits keeps UPDATE and is
|
|
225
|
+
* held to its edit. The report then reflects what happened, not what was asked.
|
|
226
|
+
*/
|
|
227
|
+
export function applyRefreshOutcomeToVerdicts(verdicts, testFile, outcome, executorImage) {
|
|
228
|
+
if (outcome.notRefreshed.length === 0)
|
|
229
|
+
return { verdicts };
|
|
230
|
+
let note;
|
|
231
|
+
const next = verdicts.map((v) => {
|
|
232
|
+
if (v.testFilePath !== testFile || v.action !== DriftAction.Update)
|
|
233
|
+
return v;
|
|
234
|
+
const remaining = (v.rebaselineSnapshots ?? []).filter((n) => !outcome.notRefreshed.includes(n));
|
|
235
|
+
const reason = `baseline refresh of ${outcome.notRefreshed.join(", ")} was not applied by ${executorImage} (its @skyramp/skyramp lacks SKYRAMP_UPDATE_SNAPSHOTS, or the name matches no toHaveScreenshot call)`;
|
|
236
|
+
if (remaining.length === 0 && v.rebaselineOnly) {
|
|
237
|
+
note = `Verdict for ${path.basename(testFile)} downgraded UPDATE → VERIFY: ${reason}. The test stays as it is; report it honestly.`;
|
|
238
|
+
const { rebaselineSnapshots: _dropped, rebaselineOnly: _only, ...rest } = v;
|
|
239
|
+
void _dropped;
|
|
240
|
+
void _only;
|
|
241
|
+
return { ...rest, action: DriftAction.Verify, rationale: `${v.rationale} [${reason}]` };
|
|
242
|
+
}
|
|
243
|
+
note = `Verdict for ${path.basename(testFile)}: ${reason}; the UPDATE is held to its spec/POM edit only.`;
|
|
244
|
+
const { rebaselineSnapshots: _dropped, ...rest } = v;
|
|
245
|
+
void _dropped;
|
|
246
|
+
return {
|
|
247
|
+
...rest,
|
|
248
|
+
...(remaining.length > 0 ? { rebaselineSnapshots: remaining } : {}),
|
|
249
|
+
rationale: `${v.rationale} [${reason}]`,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
return { verdicts: next, note };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The refresh outcome line appended to the execution result, on pass and on fail
|
|
256
|
+
* alike. Without it a stale-baseline run that could not refresh reads as a bare
|
|
257
|
+
* "Screenshot comparison failed", and the report gate then tells the agent to run
|
|
258
|
+
* with rebaselineSnapshots set — which it already did.
|
|
259
|
+
*/
|
|
260
|
+
export function describeRefreshOutcome(outcome) {
|
|
261
|
+
const parts = [];
|
|
262
|
+
if (outcome.refreshed.length > 0) {
|
|
263
|
+
parts.push(`Visual baselines refreshed: ${outcome.refreshed.join(", ")}.`);
|
|
264
|
+
}
|
|
265
|
+
if (outcome.notRefreshed.length > 0) {
|
|
266
|
+
parts.push(`Visual baselines NOT refreshed: ${outcome.notRefreshed.join(", ")} — the executor image may lack SKYRAMP_UPDATE_SNAPSHOTS support (needs @skyramp/skyramp with SKYR-4298), or the name matches no toHaveScreenshot() call in this spec. Do not report these as refreshed.`);
|
|
267
|
+
}
|
|
268
|
+
return parts.join(" ");
|
|
269
|
+
}
|
|
116
270
|
export function buildExecutionFailureText(result) {
|
|
117
271
|
const output = stripVTControlCharacters(result.output || "").trim();
|
|
118
272
|
const facts = [`status=${result.status}`];
|
|
@@ -199,8 +353,14 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
199
353
|
.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."),
|
|
200
354
|
phase: z
|
|
201
355
|
.enum(["before", "after"])
|
|
202
|
-
.
|
|
356
|
+
.default("after")
|
|
203
357
|
.describe("Execution phase for maintained tests: 'before' captures pre-edit baseline; 'after' (default) records post-edit result."),
|
|
358
|
+
rebaselineSnapshots: z
|
|
359
|
+
.array(rebaselineSnapshotNameSchema)
|
|
360
|
+
.optional()
|
|
361
|
+
.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). " +
|
|
362
|
+
"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). " +
|
|
363
|
+
"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."),
|
|
204
364
|
repository: z
|
|
205
365
|
.string()
|
|
206
366
|
.optional()
|
|
@@ -227,6 +387,24 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
227
387
|
errorResult = toolError(`testFile must be an absolute path, got: ${params.testFile}`);
|
|
228
388
|
return errorResult;
|
|
229
389
|
}
|
|
390
|
+
const rebaseline = resolveRebaselineSnapshots(params.rebaselineSnapshots, params.phase);
|
|
391
|
+
if (rebaseline.error) {
|
|
392
|
+
errorResult = toolError(rebaseline.error);
|
|
393
|
+
return errorResult;
|
|
394
|
+
}
|
|
395
|
+
const rebaselineSnapshots = rebaseline.snapshots;
|
|
396
|
+
if (rebaselineSnapshots.length > 0) {
|
|
397
|
+
if (!params.stateFile) {
|
|
398
|
+
errorResult = toolError("rebaselineSnapshots requires stateFile: the refresh is authorized against the UPDATE verdict skyramp_actions persisted there.");
|
|
399
|
+
return errorResult;
|
|
400
|
+
}
|
|
401
|
+
const authState = await StateManager.fromStatePath(params.stateFile).readRepoData(params.repository);
|
|
402
|
+
const auth = authorizeRebaseline(authState, params.testFile, rebaselineSnapshots);
|
|
403
|
+
if (auth.error) {
|
|
404
|
+
errorResult = toolError(auth.error);
|
|
405
|
+
return errorResult;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
230
408
|
// Deterministic external-test guard (SKYR-3924): this tool runs Skyramp-generated
|
|
231
409
|
// tests in the executor and cannot run a repo's native (user-written) suite, so a
|
|
232
410
|
// run on an external test only errors (e.g. pytest import/collection failure).
|
|
@@ -343,7 +521,13 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
343
521
|
playwrightSaveStoragePath: params.playwrightSaveStoragePath,
|
|
344
522
|
dockerNetwork,
|
|
345
523
|
useHostNetwork: false,
|
|
524
|
+
...(rebaselineSnapshots.length > 0 ? { rebaselineSnapshots } : {}),
|
|
346
525
|
};
|
|
526
|
+
// Identity of the requested baselines before the run, to report afterwards
|
|
527
|
+
// which ones SmartPlaywright actually rewrote (SKYR-4298).
|
|
528
|
+
const baselinesBefore = rebaselineSnapshots.length > 0
|
|
529
|
+
? readBaselineState(params.testFile, rebaselineSnapshots)
|
|
530
|
+
: {};
|
|
347
531
|
let result;
|
|
348
532
|
try {
|
|
349
533
|
result = await executionService.executeTest(execOptions, onExecutionProgress);
|
|
@@ -396,10 +580,53 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
396
580
|
// populate testResults[].videoPath — testbot uploads only the video
|
|
397
581
|
// directories the report references, so an unrecorded video is never seen.
|
|
398
582
|
await recordExecutionVideo(result, params.stateFile);
|
|
583
|
+
// Which requested baselines were actually rewritten (SKYR-4298). Reported on
|
|
584
|
+
// pass and fail alike; a refreshed PNG is a deliverable like a generated
|
|
585
|
+
// spec, so stage the spec's snapshot directory whenever one was rewritten —
|
|
586
|
+
// even on a failing run, since the report gate checks the PNG, not the
|
|
587
|
+
// status — so the eval harness commit and the artifact collector see it
|
|
588
|
+
// (production delivery adds the whole test directory anyway). No-op outside
|
|
589
|
+
// a testbot run, like every other stageGeneratedPaths call; never fails the
|
|
590
|
+
// execution.
|
|
591
|
+
let refreshOutcomeText = "";
|
|
592
|
+
if (rebaselineSnapshots.length > 0) {
|
|
593
|
+
const after = readBaselineState(params.testFile, rebaselineSnapshots);
|
|
594
|
+
const outcome = diffBaselineState(baselinesBefore, after);
|
|
595
|
+
refreshOutcomeText = describeRefreshOutcome(outcome);
|
|
596
|
+
// Stage the rewritten files themselves, never the directory: `git add` on
|
|
597
|
+
// the directory would ship anything else sitting there under one
|
|
598
|
+
// authorized baseline's authority.
|
|
599
|
+
for (const name of outcome.refreshed) {
|
|
600
|
+
for (const file of outcome.refreshedFiles[name] ?? []) {
|
|
601
|
+
try {
|
|
602
|
+
await stageGeneratedPaths(path.join(snapshotDirFor(params.testFile), file));
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
logger.warning(`Could not stage refreshed visual baseline ${file} for ${params.testFile}: ${err.message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (outcome.notRefreshed.length > 0 && params.stateFile) {
|
|
610
|
+
try {
|
|
611
|
+
const stateManager = StateManager.fromStatePath(params.stateFile);
|
|
612
|
+
const stateData = await stateManager.readRepoData(params.repository);
|
|
613
|
+
if (stateData?.maintenanceVerdicts) {
|
|
614
|
+
const reconciled = applyRefreshOutcomeToVerdicts(stateData.maintenanceVerdicts, params.testFile, outcome, EXECUTOR_DOCKER_IMAGE);
|
|
615
|
+
await stateManager.writeRepoData({ ...stateData, maintenanceVerdicts: reconciled.verdicts }, { repo: params.repository });
|
|
616
|
+
if (reconciled.note)
|
|
617
|
+
refreshOutcomeText += ` ${reconciled.note}`;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
logger.warning(`Could not reconcile the maintenance verdict with the refresh outcome: ${err.message}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const withRefreshOutcome = (text) => refreshOutcomeText ? `${text}\n\n${refreshOutcomeText}` : text;
|
|
399
626
|
// Progress is already reported by TestExecutionService
|
|
400
627
|
// Only report final status if not already at 100%
|
|
401
628
|
if (result.status !== TestExecutionStatus.Pass) {
|
|
402
|
-
errorResult = toolError(withVideoInfo(buildExecutionFailureText(result), result.videoPath));
|
|
629
|
+
errorResult = toolError(withRefreshOutcome(withVideoInfo(buildExecutionFailureText(result), result.videoPath)));
|
|
403
630
|
return errorResult;
|
|
404
631
|
}
|
|
405
632
|
// Success - progress already reported by TestExecutionService
|
|
@@ -407,7 +634,7 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
407
634
|
content: [
|
|
408
635
|
{
|
|
409
636
|
type: "text",
|
|
410
|
-
text: withVideoInfo(`Test execution result: ${stripVTControlCharacters(result.output || "")}`, result.videoPath),
|
|
637
|
+
text: withRefreshOutcome(withVideoInfo(`Test execution result: ${stripVTControlCharacters(result.output || "")}`, result.videoPath)),
|
|
411
638
|
},
|
|
412
639
|
],
|
|
413
640
|
};
|
|
@@ -490,6 +490,12 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
|
|
|
490
490
|
+ chainLint.warnings.map((w) => ` - ${w}`).join("\n")
|
|
491
491
|
: "";
|
|
492
492
|
try {
|
|
493
|
+
// The contract and integration generators create the test directory through
|
|
494
|
+
// the SDK, so a batch scenario that is the FIRST call into a new directory
|
|
495
|
+
// wrote into one that was not there and returned ENOENT. Same shape as
|
|
496
|
+
// batchMockGenerationTool.ts, and inside the try so a mkdir failure reads
|
|
497
|
+
// like a write failure.
|
|
498
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
493
499
|
fs.writeFileSync(filePath, JSON.stringify(traceRequests, null, 2), "utf8");
|
|
494
500
|
}
|
|
495
501
|
catch (error) {
|
|
@@ -34,8 +34,8 @@ const baseContractTestSchema = {
|
|
|
34
34
|
scenarioName: z
|
|
35
35
|
.string()
|
|
36
36
|
.optional()
|
|
37
|
-
.describe("The EXACT scenarioName of this test
|
|
38
|
-
"In a testbot run, ALWAYS pass it verbatim from
|
|
37
|
+
.describe("The EXACT scenarioName of this test as you registered it with skyramp_register_test_plan. " +
|
|
38
|
+
"In a testbot run, ALWAYS pass it verbatim from your registered plan — it is the primary key the " +
|
|
39
39
|
"plan gate matches on, so generation is admitted even when the endpoint path drifts from the plan's. " +
|
|
40
40
|
"Omit in standalone/IDE usage (no registered plan)."),
|
|
41
41
|
...baseTestSchema,
|
|
@@ -223,7 +223,7 @@ ${this.buildSampleDataSection(params)}
|
|
|
223
223
|
${step5}
|
|
224
224
|
|
|
225
225
|
**What NOT to do — any of these is a violation:**
|
|
226
|
-
- Do NOT change function signatures, method names, class names,
|
|
226
|
+
- Do NOT change function signatures, method names, class names, or variable names. Add an import only when the code you add needs it.
|
|
227
227
|
- Do NOT add, remove, or reorder any functions, classes, or test cases.
|
|
228
228
|
- Do NOT change assertion logic, HTTP methods, URLs, headers, or status code checks.
|
|
229
229
|
- Do NOT reformat, reorder, or rewrite any code outside the identified JSON/dict literals.
|
|
@@ -7,7 +7,7 @@ export type PlanGuardQuery = PlanMatchQuery;
|
|
|
7
7
|
export { pathFromEndpointURL } from "../../utils/urlPath.js";
|
|
8
8
|
/**
|
|
9
9
|
* Returns null when the generation call is allowed, or a human-readable error
|
|
10
|
-
* message (for `toolError`) when it must be rejected because it
|
|
11
|
-
*
|
|
10
|
+
* message (for `toolError`) when it must be rejected because it matches no
|
|
11
|
+
* planned test in the registered plan.
|
|
12
12
|
*/
|
|
13
13
|
export declare function guardScenarioApproved(query: PlanGuardQuery): Promise<string | null>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Hard gate wired into the generation tools' handlers.
|
|
3
|
+
*
|
|
4
|
+
* Conditional on an ACTIVE run whose agent has registered a plan: standalone use
|
|
5
|
+
* and runs with no plan are unaffected. The report gate refuses a missing plan.
|
|
6
6
|
*/
|
|
7
7
|
import { resolveRunStatePath, StateManager } from "../../utils/AnalysisStateManager.js";
|
|
8
|
-
import {
|
|
8
|
+
import { matchesPlanCandidates, nameMatchedEndpointMismatches, nameMatchedTypeMismatches, } from "../../utils/planMatchKeys.js";
|
|
9
9
|
import { TOOL_REGISTER_TEST_PLAN } from "../../toolNames.js";
|
|
10
10
|
/** Re-exported for the generation tools that already import it from here.
|
|
11
11
|
* The implementation moved to utils/urlPath.ts (SKYR-4127) once workspace
|
|
@@ -14,8 +14,8 @@ import { TOOL_REGISTER_TEST_PLAN } from "../../toolNames.js";
|
|
|
14
14
|
export { pathFromEndpointURL } from "../../utils/urlPath.js";
|
|
15
15
|
/**
|
|
16
16
|
* Returns null when the generation call is allowed, or a human-readable error
|
|
17
|
-
* message (for `toolError`) when it must be rejected because it
|
|
18
|
-
*
|
|
17
|
+
* message (for `toolError`) when it must be rejected because it matches no
|
|
18
|
+
* planned test in the registered plan.
|
|
19
19
|
*/
|
|
20
20
|
export async function guardScenarioApproved(query) {
|
|
21
21
|
const statePath = resolveRunStatePath();
|
|
@@ -29,18 +29,78 @@ export async function guardScenarioApproved(query) {
|
|
|
29
29
|
// Unreadable/corrupt state must never block generation on an infra hiccup.
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
32
|
+
const plan = stateData?.plan;
|
|
33
|
+
if (!plan)
|
|
34
34
|
return null;
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
35
|
+
// The plan is read off disk, so its declared array type holds only on the
|
|
36
|
+
// validated path. An unreadable `plannedTests` is no evidence a scenario went
|
|
37
|
+
// unplanned, so it ALLOWS, as the report gate does; becoming `[]` refused every
|
|
38
|
+
// call. An EMPTY array is not that state — it says no test was warranted.
|
|
39
|
+
if (!Array.isArray(plan.plannedTests))
|
|
38
40
|
return null;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
+
const plannedTests = plan.plannedTests;
|
|
42
|
+
if (matchesPlanCandidates(plannedTests, query))
|
|
43
|
+
return null;
|
|
44
|
+
// A planned test whose NAME and testType match is not an unplanned scenario — it is
|
|
45
|
+
// the planned one, called against an endpoint its steps do not name. That case
|
|
46
|
+
// gets its own message naming the step that disagreed.
|
|
47
|
+
const nearMisses = nameMatchedEndpointMismatches(plannedTests, query);
|
|
48
|
+
if (nearMisses.length > 0) {
|
|
49
|
+
// The plannedTestId is printed with the name because two planned tests can share
|
|
50
|
+
// a scenarioName, and a missing method or path is printed as such: the
|
|
51
|
+
// disagreement is then a field the plan left empty, not an endpoint the
|
|
52
|
+
// agent invented, and the message must not say otherwise.
|
|
53
|
+
const plannedSteps = nearMisses
|
|
54
|
+
.flatMap((item) => {
|
|
55
|
+
const steps = Array.isArray(item?.scenario?.steps) ? item.scenario.steps : [];
|
|
56
|
+
return steps
|
|
57
|
+
.filter((step) => step?.method || step?.path)
|
|
58
|
+
.map((step) => ` - [${item?.scenario?.testType ?? "?"}] ${item?.scenario?.scenarioName ?? "(unnamed)"} ` +
|
|
59
|
+
`(${item?.plannedTestId}): ${step?.method ?? "(no method)"} ${step?.path ?? "(no path)"}`);
|
|
60
|
+
})
|
|
61
|
+
.join("\n");
|
|
62
|
+
const head = `Scenario "${query.scenarioName}" IS in the plan you registered with ${TOOL_REGISTER_TEST_PLAN} ` +
|
|
63
|
+
`(registration ${plan.registrationNumber}), but `;
|
|
64
|
+
const called = `You called: ${query.method || "any method"} ${query.path}\n`;
|
|
65
|
+
// No step at all is a different refusal: there is nothing to retry
|
|
66
|
+
// generation against, so asking for that would send the agent in a circle.
|
|
67
|
+
if (!plannedSteps) {
|
|
68
|
+
return (head +
|
|
69
|
+
`it holds no step, so there is nothing to check this endpoint against.\n\n` +
|
|
70
|
+
called +
|
|
71
|
+
`Call ${TOOL_REGISTER_TEST_PLAN} again with the step this scenario tests, then retry generation.`);
|
|
72
|
+
}
|
|
73
|
+
return (head +
|
|
74
|
+
`no step of it agrees with the endpoint of this call.\n\n` +
|
|
75
|
+
called +
|
|
76
|
+
`The plan's steps for it:\n${plannedSteps}\n\n` +
|
|
77
|
+
`The two have to agree on the method and the path. Retry generation against the step the plan holds, or call ` +
|
|
78
|
+
`${TOOL_REGISTER_TEST_PLAN} again with the step corrected to the endpoint you mean to test.`);
|
|
79
|
+
}
|
|
80
|
+
// The plan holds this scenario under a DIFFERENT test type, so "not in the plan"
|
|
81
|
+
// is the same wrong message the endpoint near miss fixed. Neither the step nor
|
|
82
|
+
// the planned test is wrong: the two sides disagree about the KIND of test.
|
|
83
|
+
const typeMisses = nameMatchedTypeMismatches(plannedTests, query);
|
|
84
|
+
if (typeMisses.length > 0) {
|
|
85
|
+
// The plannedTestId is printed with the type because two planned tests can share
|
|
86
|
+
// a scenarioName, so the agent can see which entry to change.
|
|
87
|
+
const plannedTypes = typeMisses
|
|
88
|
+
.map((item) => ` - [${item?.scenario?.testType ?? "?"}] ${item?.plannedTestId}`)
|
|
89
|
+
.join("\n");
|
|
90
|
+
return (`Scenario "${query.scenarioName}" IS in the plan you registered with ${TOOL_REGISTER_TEST_PLAN} ` +
|
|
91
|
+
`(registration ${plan.registrationNumber}), under a different test type.\n\n` +
|
|
92
|
+
`The plan holds it as:\n${plannedTypes}\n\n` +
|
|
93
|
+
`You called the ${query.testType ?? "(untyped)"} generator.\n\n` +
|
|
94
|
+
`Either call the generator for the type the plan holds, or call ${TOOL_REGISTER_TEST_PLAN} again with the type you mean and retry. ` +
|
|
95
|
+
`Changing the plan is only the right move when the plan has the type wrong.`);
|
|
96
|
+
}
|
|
97
|
+
const outstanding = plannedTests.length > 0
|
|
98
|
+
? plannedTests
|
|
99
|
+
.map((item) => ` - [${item?.scenario?.testType ?? "?"}] ${item?.scenario?.scenarioName ?? item?.plannedTestId}`)
|
|
100
|
+
.join("\n")
|
|
41
101
|
: " (none)";
|
|
42
|
-
return (`This scenario is not in the
|
|
43
|
-
`
|
|
44
|
-
`Call ${TOOL_REGISTER_TEST_PLAN}
|
|
45
|
-
`exact scenarioName/testType/endpoint
|
|
102
|
+
return (`This scenario is not in the plan you registered with ${TOOL_REGISTER_TEST_PLAN} (registration ${plan.registrationNumber}).\n\n` +
|
|
103
|
+
`Planned planned tests:\n${outstanding}\n\n` +
|
|
104
|
+
`Call ${TOOL_REGISTER_TEST_PLAN} again with this planned test included in \`planned tests\`, then retry generation using the ` +
|
|
105
|
+
`exact scenarioName/testType/endpoint you registered.`);
|
|
46
106
|
}
|
|
@@ -160,10 +160,6 @@ const baseOneClickSchema = z.object({
|
|
|
160
160
|
.string()
|
|
161
161
|
.optional()
|
|
162
162
|
.describe("Path or URL to OpenAPI/Swagger schema. If omitted, use schema from workspace or analysis."),
|
|
163
|
-
topN: z
|
|
164
|
-
.number()
|
|
165
|
-
.optional()
|
|
166
|
-
.describe("Number of top recommended test types to generate (e.g. 3–5). Default is 3. Used by full_repo_scan_recommend_generate_and_execute_top_n_tests."),
|
|
167
163
|
token: z
|
|
168
164
|
.string()
|
|
169
165
|
.optional()
|
|
@@ -314,7 +310,6 @@ export function registerOneClickTool(server) {
|
|
|
314
310
|
repositoryPath: params.repositoryPath,
|
|
315
311
|
endpointURL: params.endpointURL,
|
|
316
312
|
apiSchema: params.apiSchema,
|
|
317
|
-
topN: params.topN ?? 3,
|
|
318
313
|
token: params.token ?? "",
|
|
319
314
|
baseBranch: params.baseBranch,
|
|
320
315
|
servicesToMock: params.servicesToMock,
|