@skyramp/mcp 0.3.8 → 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ScreenRouteCandidate } from "../../utils/screenRoutes.js";
|
|
3
|
+
/** Where a bound route parameter came from. `unresolved` never carries a value. */
|
|
4
|
+
export type ParamSource = "currentUrl" | "blueprint" | "unresolved";
|
|
5
|
+
export interface ResolvedParam {
|
|
6
|
+
value: string;
|
|
7
|
+
from: ParamSource;
|
|
8
|
+
}
|
|
9
|
+
export interface ScreenCandidate {
|
|
10
|
+
/** The route with its known parameters filled in, prefixed by `baseUrl` when one
|
|
11
|
+
* is known. Any parameter still in `needs` is left in its pattern spelling. */
|
|
12
|
+
url: string;
|
|
13
|
+
routePattern: string;
|
|
14
|
+
params: Record<string, ResolvedParam>;
|
|
15
|
+
/** Route entry file first, changed file last — the evidence for `routePattern`. */
|
|
16
|
+
mountChain: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ResolveScreenResult {
|
|
19
|
+
status: "resolved" | "unmounted" | "unknown";
|
|
20
|
+
candidates: ScreenCandidate[];
|
|
21
|
+
/** Route patterns ruled out. When the browser's page is known this is that page's
|
|
22
|
+
* own route: the result then reads as a correction of where the browser is. */
|
|
23
|
+
notOn: string[];
|
|
24
|
+
/** Route parameters nothing could fill. */
|
|
25
|
+
needs: string[];
|
|
26
|
+
summary: string;
|
|
27
|
+
}
|
|
28
|
+
/** Where the browser is, as this server saw it. */
|
|
29
|
+
export interface BrowserLocation {
|
|
30
|
+
url: string;
|
|
31
|
+
source: "currentUrl" | "blueprint";
|
|
32
|
+
}
|
|
33
|
+
/** The path half of a url, or the value itself when it is already a path. */
|
|
34
|
+
export declare function urlPath(raw: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* The parameters the browser's current page supplies, and the route it is on.
|
|
37
|
+
*
|
|
38
|
+
* The binding runs against the CURRENT page's own pattern, and the values carry
|
|
39
|
+
* across by NAME. In the motivating run the browser sat on
|
|
40
|
+
* `/organizations/8f2e6b9f/projects/secret-management/1eb91367/overview`, whose
|
|
41
|
+
* pattern names `$orgId` and `$projectId`; the target route names those two and
|
|
42
|
+
* `$envSlug`, so two are known and one is not.
|
|
43
|
+
*
|
|
44
|
+
* A url matching several patterns is ambiguous, and the most literal one wins: a
|
|
45
|
+
* static segment is a stronger claim about the page than a parameter that accepts
|
|
46
|
+
* anything.
|
|
47
|
+
*/
|
|
48
|
+
export declare function bindFromLocation(location: BrowserLocation | undefined, patterns: string[]): {
|
|
49
|
+
pattern: string;
|
|
50
|
+
values: Record<string, string>;
|
|
51
|
+
source: ParamSource;
|
|
52
|
+
} | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Turn the resolver's three-state answer into the tool's, with parameters bound
|
|
55
|
+
* from wherever the browser is. Pure: every input is passed in, so the shape of the
|
|
56
|
+
* answer is testable without a repository or a browser.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildResolveScreenResult(input: {
|
|
59
|
+
changedFile: string;
|
|
60
|
+
candidates: ScreenRouteCandidate[] | undefined;
|
|
61
|
+
routePatterns: string[];
|
|
62
|
+
location?: BrowserLocation;
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
}): ResolveScreenResult;
|
|
65
|
+
/**
|
|
66
|
+
* The origin the UI is served from, as `.skyramp/workspace.yml` records it.
|
|
67
|
+
*
|
|
68
|
+
* There is no explicit "this one is the UI" marker in the workspace schema, so the
|
|
69
|
+
* service that runs Playwright is the proxy for it; failing that, an unambiguous
|
|
70
|
+
* single service with a baseUrl. Ambiguity yields nothing rather than a guess —
|
|
71
|
+
* the caller can pass `baseUrl`, and a route path without an origin is still a
|
|
72
|
+
* correct answer.
|
|
73
|
+
*/
|
|
74
|
+
export declare function workspaceUiBaseUrl(repositoryPath: string): Promise<string | undefined>;
|
|
75
|
+
export declare function registerResolveScreenTool(server: McpServer): void;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skyramp_resolve_screen` — which URL renders this changed file, and how to get
|
|
3
|
+
* there from where the browser already is (SKYR-4341).
|
|
4
|
+
*
|
|
5
|
+
* The dead end this tool ends: an agent recording a UI trace finds the element it
|
|
6
|
+
* is looking for is not in the snapshot, and has nowhere to go. In run 33999747382
|
|
7
|
+
* it went to `browser_evaluate`, twenty-nine times, on a page that never rendered
|
|
8
|
+
* the changed component, and produced no test. The page was wrong, not the
|
|
9
|
+
* selector, and nothing in the run could say so.
|
|
10
|
+
*
|
|
11
|
+
* THE TOOL TAKES NO `currentUrl`. This server forwards every `browser_navigate`
|
|
12
|
+
* and records every blueprint capture, so it knows which page the browser is on;
|
|
13
|
+
* asking the agent instead would take the answer from the party this tool exists
|
|
14
|
+
* to correct. An agent-supplied url can be stale or invented, and the correction
|
|
15
|
+
* would then be built on the mistake.
|
|
16
|
+
*
|
|
17
|
+
* It reports what the resolver knows and never more: `resolved` with routes,
|
|
18
|
+
* `unmounted` when nothing renders the file, `unknown` when the scan cannot speak.
|
|
19
|
+
* A route parameter nothing could fill goes in `needs` — never a guessed value,
|
|
20
|
+
* because a plausible wrong id sends the browser to an empty page that looks real.
|
|
21
|
+
*/
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { TOOL_RESOLVE_SCREEN } from "../../toolNames.js";
|
|
25
|
+
import { logger } from "../../utils/logger.js";
|
|
26
|
+
import { lastKnownBrowserUrl } from "../../playwright/blueprintDigestStore.js";
|
|
27
|
+
import { readWorkspaceServices } from "../../prompts/prompt-utils.js";
|
|
28
|
+
import { createScreenRouteLookup } from "../../utils/screenRoutes.js";
|
|
29
|
+
// ── Route patterns ──────────────────────────────────────────────────────────
|
|
30
|
+
/** A pattern segment, in the four spellings the frameworks this run meets use:
|
|
31
|
+
* TanStack `$id`, React/Vue `:id`, Next `[id]`, OpenAPI-ish `{id}`. Mirrors the
|
|
32
|
+
* `screenRoute` verifier — the two answer the same question about the same
|
|
33
|
+
* patterns, and disagreeing about what a parameter looks like would let one pass
|
|
34
|
+
* what the other objects to. */
|
|
35
|
+
function parameterName(segment) {
|
|
36
|
+
if (segment.startsWith("$"))
|
|
37
|
+
return segment.slice(1) || undefined;
|
|
38
|
+
if (segment.startsWith(":"))
|
|
39
|
+
return segment.slice(1) || undefined;
|
|
40
|
+
if (segment.startsWith("[") && segment.endsWith("]"))
|
|
41
|
+
return segment.slice(1, -1).replace(/^\.\.\./, "") || undefined;
|
|
42
|
+
if (segment.startsWith("{") && segment.endsWith("}"))
|
|
43
|
+
return segment.slice(1, -1) || undefined;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function segments(value) {
|
|
47
|
+
return value.split("/").filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
/** The path half of a url, or the value itself when it is already a path. */
|
|
50
|
+
export function urlPath(raw) {
|
|
51
|
+
const value = (raw ?? "").trim();
|
|
52
|
+
if (!value)
|
|
53
|
+
return "";
|
|
54
|
+
try {
|
|
55
|
+
return new URL(value).pathname.replace(/\/+$/, "") || "/";
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return value.startsWith("/") ? value.split(/[?#]/)[0].replace(/\/+$/, "") || "/" : "";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function origin(raw) {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(raw).origin;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Bind a pattern's parameters from a concrete path, or nothing when the path is
|
|
70
|
+
* not that pattern filled in. Depth agrees or nothing does: a parameter matches
|
|
71
|
+
* one segment and never spans two. */
|
|
72
|
+
function bind(pattern, concretePath) {
|
|
73
|
+
const wanted = segments(pattern);
|
|
74
|
+
const actual = segments(concretePath);
|
|
75
|
+
if (wanted.length !== actual.length)
|
|
76
|
+
return undefined;
|
|
77
|
+
const bound = {};
|
|
78
|
+
for (let i = 0; i < wanted.length; i++) {
|
|
79
|
+
const name = parameterName(wanted[i]);
|
|
80
|
+
if (name)
|
|
81
|
+
bound[name] = actual[i];
|
|
82
|
+
else if (wanted[i] !== actual[i])
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return bound;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The parameters the browser's current page supplies, and the route it is on.
|
|
89
|
+
*
|
|
90
|
+
* The binding runs against the CURRENT page's own pattern, and the values carry
|
|
91
|
+
* across by NAME. In the motivating run the browser sat on
|
|
92
|
+
* `/organizations/8f2e6b9f/projects/secret-management/1eb91367/overview`, whose
|
|
93
|
+
* pattern names `$orgId` and `$projectId`; the target route names those two and
|
|
94
|
+
* `$envSlug`, so two are known and one is not.
|
|
95
|
+
*
|
|
96
|
+
* A url matching several patterns is ambiguous, and the most literal one wins: a
|
|
97
|
+
* static segment is a stronger claim about the page than a parameter that accepts
|
|
98
|
+
* anything.
|
|
99
|
+
*/
|
|
100
|
+
export function bindFromLocation(location, patterns) {
|
|
101
|
+
if (!location)
|
|
102
|
+
return undefined;
|
|
103
|
+
const concretePath = urlPath(location.url);
|
|
104
|
+
if (!concretePath)
|
|
105
|
+
return undefined;
|
|
106
|
+
let best;
|
|
107
|
+
for (const pattern of patterns) {
|
|
108
|
+
const values = bind(pattern, concretePath);
|
|
109
|
+
if (!values)
|
|
110
|
+
continue;
|
|
111
|
+
const literals = segments(pattern).filter((segment) => !parameterName(segment)).length;
|
|
112
|
+
if (!best || literals > best.literals)
|
|
113
|
+
best = { pattern, values, literals };
|
|
114
|
+
}
|
|
115
|
+
return best ? { pattern: best.pattern, values: best.values, source: location.source } : undefined;
|
|
116
|
+
}
|
|
117
|
+
/** The pattern with its bound parameters substituted, prefixed by the origin when
|
|
118
|
+
* one is known. An unbound parameter is left in its own spelling rather than
|
|
119
|
+
* dropped: a url missing a segment silently names a different page. */
|
|
120
|
+
function buildUrl(pattern, params, baseUrl) {
|
|
121
|
+
const filled = segments(pattern)
|
|
122
|
+
.map((segment) => {
|
|
123
|
+
const name = parameterName(segment);
|
|
124
|
+
const bound = name ? params[name] : undefined;
|
|
125
|
+
return bound && bound.from !== "unresolved" ? bound.value : segment;
|
|
126
|
+
})
|
|
127
|
+
.join("/");
|
|
128
|
+
const pathPart = "/" + filled;
|
|
129
|
+
const base = baseUrl ? baseUrl.replace(/\/+$/, "") : "";
|
|
130
|
+
return base ? base + pathPart : pathPart;
|
|
131
|
+
}
|
|
132
|
+
// ── The answer ──────────────────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Turn the resolver's three-state answer into the tool's, with parameters bound
|
|
135
|
+
* from wherever the browser is. Pure: every input is passed in, so the shape of the
|
|
136
|
+
* answer is testable without a repository or a browser.
|
|
137
|
+
*/
|
|
138
|
+
export function buildResolveScreenResult(input) {
|
|
139
|
+
const { changedFile, candidates, routePatterns, location, baseUrl } = input;
|
|
140
|
+
const here = bindFromLocation(location, routePatterns);
|
|
141
|
+
if (candidates === undefined) {
|
|
142
|
+
return {
|
|
143
|
+
status: "unknown",
|
|
144
|
+
candidates: [],
|
|
145
|
+
notOn: [],
|
|
146
|
+
needs: [],
|
|
147
|
+
summary: `No route table this server can read covers ${changedFile}, so it cannot say which page renders it. ` +
|
|
148
|
+
"Find the page yourself; do not treat this as evidence that nothing renders the file.",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (candidates.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
status: "unmounted",
|
|
154
|
+
candidates: [],
|
|
155
|
+
// The current page is ruled out along with every other: nothing mounts it.
|
|
156
|
+
notOn: here ? [here.pattern] : [],
|
|
157
|
+
needs: [],
|
|
158
|
+
summary: `No route in this repository renders ${changedFile}. It is not reachable in the running app, ` +
|
|
159
|
+
"so a browser recording cannot exercise it. Say so in one line and keep the planned test out of the browser.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const needs = new Set();
|
|
163
|
+
const resolved = candidates.map((candidate) => {
|
|
164
|
+
const params = {};
|
|
165
|
+
for (const segment of segments(candidate.routePattern)) {
|
|
166
|
+
const name = parameterName(segment);
|
|
167
|
+
if (!name)
|
|
168
|
+
continue;
|
|
169
|
+
const value = here?.values[name];
|
|
170
|
+
if (value !== undefined)
|
|
171
|
+
params[name] = { value, from: here?.source ?? "unresolved" };
|
|
172
|
+
else {
|
|
173
|
+
params[name] = { value: "", from: "unresolved" };
|
|
174
|
+
needs.add(name);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
url: buildUrl(candidate.routePattern, params, baseUrl),
|
|
179
|
+
routePattern: candidate.routePattern,
|
|
180
|
+
params,
|
|
181
|
+
mountChain: candidate.mountChain,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
const mounts = new Set(resolved.map((candidate) => candidate.routePattern));
|
|
185
|
+
const notOn = here && !mounts.has(here.pattern) ? [here.pattern] : [];
|
|
186
|
+
const lines = [];
|
|
187
|
+
if (notOn.length > 0) {
|
|
188
|
+
lines.push(`The browser is on ${location?.url}, which is ${notOn[0]} — that page does not render ${changedFile}.`);
|
|
189
|
+
lines.push(`Navigate to ${resolved[0].url} instead.`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
lines.push(`${changedFile} is rendered on ${resolved.map((c) => c.routePattern).join("; ")}.`);
|
|
193
|
+
}
|
|
194
|
+
if (needs.size > 0) {
|
|
195
|
+
lines.push(`No value is known for ${[...needs].join(", ")}. Read ${needs.size === 1 ? "it" : "them"} off the app with ` +
|
|
196
|
+
"`browser_snapshot`: the value shows up as a link or a list entry in the ARIA tree, so open the parent " +
|
|
197
|
+
"page and snapshot it if the current one does not hold it. Do not dig for it with `browser_evaluate`, " +
|
|
198
|
+
"and do not invent one — a plausible wrong id renders an empty page that looks real.");
|
|
199
|
+
}
|
|
200
|
+
lines.push("`mountChain` is the import path from the route file to the changed file: check it rather than trust it.");
|
|
201
|
+
return { status: "resolved", candidates: resolved, notOn, needs: [...needs], summary: lines.join(" ") };
|
|
202
|
+
}
|
|
203
|
+
// ── Workspace default for baseUrl ───────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* The origin the UI is served from, as `.skyramp/workspace.yml` records it.
|
|
206
|
+
*
|
|
207
|
+
* There is no explicit "this one is the UI" marker in the workspace schema, so the
|
|
208
|
+
* service that runs Playwright is the proxy for it; failing that, an unambiguous
|
|
209
|
+
* single service with a baseUrl. Ambiguity yields nothing rather than a guess —
|
|
210
|
+
* the caller can pass `baseUrl`, and a route path without an origin is still a
|
|
211
|
+
* correct answer.
|
|
212
|
+
*/
|
|
213
|
+
export async function workspaceUiBaseUrl(repositoryPath) {
|
|
214
|
+
try {
|
|
215
|
+
const services = (await readWorkspaceServices(repositoryPath));
|
|
216
|
+
const withUrl = services.filter((service) => typeof service?.api?.baseUrl === "string" && service.api.baseUrl);
|
|
217
|
+
const playwright = withUrl.filter((service) => service.framework === "playwright");
|
|
218
|
+
const chosen = playwright.length === 1 ? playwright[0] : withUrl.length === 1 ? withUrl[0] : undefined;
|
|
219
|
+
return chosen ? origin(chosen.api?.baseUrl) : undefined;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ── Registration ────────────────────────────────────────────────────────────
|
|
226
|
+
const inputSchema = {
|
|
227
|
+
repositoryPath: z.string().describe("Absolute path to the repository checkout that holds the frontend file."),
|
|
228
|
+
changedFile: z
|
|
229
|
+
.string()
|
|
230
|
+
.describe("Repository-relative path of the frontend file the test is about — the file the diff changed, " +
|
|
231
|
+
"not the page you think renders it."),
|
|
232
|
+
baseUrl: z
|
|
233
|
+
.string()
|
|
234
|
+
.optional()
|
|
235
|
+
.describe("Origin the UI is served from, e.g. 'http://localhost:3000'. Defaults to the UI service's " +
|
|
236
|
+
"`api.baseUrl` in `.skyramp/workspace.yml`; when neither is known the returned urls are route paths."),
|
|
237
|
+
};
|
|
238
|
+
const outputSchema = {
|
|
239
|
+
status: z
|
|
240
|
+
.enum(["resolved", "unmounted", "unknown"])
|
|
241
|
+
.describe("resolved — the routes in `candidates` render the file. unmounted — no route renders it, so no browser " +
|
|
242
|
+
"recording can reach it. unknown — no route table could be read; this says nothing about the file."),
|
|
243
|
+
candidates: z
|
|
244
|
+
.array(z.object({
|
|
245
|
+
url: z.string(),
|
|
246
|
+
routePattern: z.string(),
|
|
247
|
+
params: z.record(z.object({ value: z.string(), from: z.enum(["currentUrl", "blueprint", "unresolved"]) })),
|
|
248
|
+
mountChain: z.array(z.string()),
|
|
249
|
+
}))
|
|
250
|
+
.describe("Each route that renders the file, with the import chain from its route file down to the file."),
|
|
251
|
+
notOn: z
|
|
252
|
+
.array(z.string())
|
|
253
|
+
.describe("The route the browser is currently on, when it is not one that renders the file."),
|
|
254
|
+
needs: z.array(z.string()).describe("Route parameters no known value fills. Read them off the app; never invent one."),
|
|
255
|
+
summary: z.string().describe("The same answer in one paragraph."),
|
|
256
|
+
};
|
|
257
|
+
const DESCRIPTION = "Answer which page of the running app renders a changed frontend file, and give the url to open. " +
|
|
258
|
+
"Call it BEFORE hunting for an element that is not in the snapshot: the usual cause is that the browser is " +
|
|
259
|
+
"on the wrong page, and no amount of browser_evaluate finds an element the page never rendered. " +
|
|
260
|
+
"Returns status 'resolved' with candidate urls, 'unmounted' when no route renders the file at all " +
|
|
261
|
+
"(skip the browser and say so), or 'unknown' when no route table could be read (find the page yourself). " +
|
|
262
|
+
"The current page is read from this server's own browser session — do not pass it.";
|
|
263
|
+
export function registerResolveScreenTool(server) {
|
|
264
|
+
server.registerTool(TOOL_RESOLVE_SCREEN, { description: DESCRIPTION, inputSchema, outputSchema }, async (params) => {
|
|
265
|
+
const repositoryPath = String(params.repositoryPath ?? "").trim();
|
|
266
|
+
const changedFile = String(params.changedFile ?? "").trim();
|
|
267
|
+
const location = lastKnownBrowserUrl();
|
|
268
|
+
const baseUrl = (typeof params.baseUrl === "string" && params.baseUrl.trim()
|
|
269
|
+
? origin(params.baseUrl.trim()) || params.baseUrl.trim()
|
|
270
|
+
: undefined) ??
|
|
271
|
+
(await workspaceUiBaseUrl(repositoryPath)) ??
|
|
272
|
+
(location ? origin(location.url) : undefined);
|
|
273
|
+
const lookup = createScreenRouteLookup(path.resolve(repositoryPath));
|
|
274
|
+
const result = buildResolveScreenResult({
|
|
275
|
+
changedFile,
|
|
276
|
+
candidates: lookup.candidatesFor(changedFile),
|
|
277
|
+
routePatterns: lookup.routePatterns(),
|
|
278
|
+
location,
|
|
279
|
+
baseUrl,
|
|
280
|
+
});
|
|
281
|
+
logger.info("skyramp_resolve_screen answered", {
|
|
282
|
+
changedFile,
|
|
283
|
+
status: result.status,
|
|
284
|
+
routes: result.candidates.length,
|
|
285
|
+
needs: result.needs.length,
|
|
286
|
+
});
|
|
287
|
+
return { structuredContent: { ...result }, content: [{ type: "text", text: result.summary }] };
|
|
288
|
+
});
|
|
289
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** What is kept from one `browser_blueprint` capture so a plan-time check can ask
|
|
2
|
+
* whether an element the agent named was really on the page. A DIGEST, never the
|
|
3
|
+
* capture: the largest real one is 26,542 bytes and a run takes many, so only the
|
|
4
|
+
* fields an element can be recognised by are kept. */
|
|
5
|
+
export interface BlueprintElementDigest {
|
|
6
|
+
/** ARIA role, as the capture spells it. */
|
|
7
|
+
role: string;
|
|
8
|
+
/** The element's accessible name. For a REPEATING element the capture carries
|
|
9
|
+
* `accessibleNameTemplate` instead — one entry is kept for the template and one
|
|
10
|
+
* per row with `{param}` filled in, so either spelling is recognised. */
|
|
11
|
+
accessibleName: string;
|
|
12
|
+
/** `data-testid` value. Absent when the element has none — SKYR-4208 removed
|
|
13
|
+
* null-valued keys from the capture, so an absence is what arrives. */
|
|
14
|
+
testId?: string;
|
|
15
|
+
/** Unique `id` attribute value, absent when the element has none. */
|
|
16
|
+
stableId?: string;
|
|
17
|
+
/** Surrounding row text for an element inside a repeating section. It is the
|
|
18
|
+
* only thing that separates two rows whose rendered names collide. */
|
|
19
|
+
contextText?: string[];
|
|
20
|
+
}
|
|
21
|
+
/** One capture, keyed by the page it was taken from. */
|
|
22
|
+
export interface BlueprintCaptureDigest {
|
|
23
|
+
/** The page URL the capture reports. A repeat capture at the same URL carries
|
|
24
|
+
* no URL of its own, so it inherits the one before it. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** The capture's own page hash. Stored so a stale citation can be recognised
|
|
27
|
+
* later; v0 matching does not narrow by it — see `uiElementGrounded`. */
|
|
28
|
+
pageHash?: string;
|
|
29
|
+
elements: BlueprintElementDigest[];
|
|
30
|
+
/** Whether the element cap cut this capture. An ungrounded objection says so,
|
|
31
|
+
* because "no capture holds that element" is not a fact about a page whose
|
|
32
|
+
* elements were only partly kept. */
|
|
33
|
+
elementsTruncated?: boolean;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|