@skyramp/mcp 0.2.7 → 0.2.8
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/index.js +5 -2
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
- package/build/prompts/sut-setup/shared.d.ts +39 -0
- package/build/prompts/sut-setup/shared.js +132 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
- package/build/prompts/testbot/testbot-prompts.js +113 -22
- package/build/prompts/testbot/testbot-prompts.test.js +95 -45
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.js +2 -2
- package/build/services/TestDiscoveryService.d.ts +2 -2
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestExecutionService.d.ts +10 -1
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.js +306 -0
- package/build/tools/executeSkyrampTestTool.js +26 -7
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/submitReportTool.d.ts +10 -0
- package/build/tools/submitReportTool.js +106 -39
- package/build/tools/submitReportTool.test.js +296 -50
- package/build/tools/test-management/actionsTool.js +21 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
- package/build/tools/test-management/analyzeChangesTool.js +106 -41
- package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
- package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
- package/build/tools/workspace/serviceUpsert.d.ts +12 -0
- package/build/tools/workspace/serviceUpsert.js +23 -0
- package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
- package/build/tools/workspace/serviceUpsert.test.js +50 -0
- package/build/types/TestAnalysis.d.ts +2 -1
- package/build/types/TestExecution.d.ts +12 -2
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestbotReport.d.ts +68 -0
- package/build/types/TestbotReport.js +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.js +3 -0
- package/build/utils/AnalysisStateManager.d.ts +61 -1
- package/build/utils/AnalysisStateManager.js +144 -3
- package/build/utils/AnalysisStateManager.test.js +98 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/routeParsers.d.ts +0 -10
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/utils.d.ts +7 -1
- package/build/utils/utils.js +10 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +10 -10
- package/build/workspace/workspace.js +7 -7
- package/build/workspace/workspace.test.js +6 -6
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -142,7 +142,7 @@ export function inferResourceRelationships(endpoints) {
|
|
|
142
142
|
}
|
|
143
143
|
return relationships;
|
|
144
144
|
}
|
|
145
|
-
export function draftScenariosFromEndpoints(endpoints, newEndpoints = [], wsAuthType, options = {}) {
|
|
145
|
+
export function draftScenariosFromEndpoints(endpoints, newEndpoints = [], wsAuthType, options = {}, removedEndpoints = []) {
|
|
146
146
|
const scenarios = [];
|
|
147
147
|
const resourceGroups = new Map();
|
|
148
148
|
for (const ep of endpoints) {
|
|
@@ -191,6 +191,28 @@ export function draftScenariosFromEndpoints(endpoints, newEndpoints = [], wsAuth
|
|
|
191
191
|
}
|
|
192
192
|
scenarios.push(...draftDiffDirectScenarios(newEndpoints, resourceGroups, wsAuthType));
|
|
193
193
|
scenarios.push(...draftAttackSurfaceExpansionScenarios(endpoints, options.changedEndpoints ?? [], wsAuthType, options.securityRelevantDiff ?? false));
|
|
194
|
+
// 404-guard scenarios for removed endpoints — absence assertions confirming
|
|
195
|
+
// the endpoint is gone. Single-step contract tests, no chaining needed.
|
|
196
|
+
for (const ep of removedEndpoints) {
|
|
197
|
+
scenarios.push({
|
|
198
|
+
scenarioName: `verify-removed-${ep.method.toLowerCase()}-${ep.path.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "")}`,
|
|
199
|
+
description: `Verify ${ep.method} ${ep.path} returns 404 — endpoint was removed in this PR`,
|
|
200
|
+
category: "breaking_change",
|
|
201
|
+
priority: "high",
|
|
202
|
+
steps: [{
|
|
203
|
+
order: 1,
|
|
204
|
+
method: ep.method,
|
|
205
|
+
path: ep.path,
|
|
206
|
+
description: `${ep.method} ${ep.path} → 404 (endpoint removed)`,
|
|
207
|
+
interactionType: "error",
|
|
208
|
+
expectedStatusCode: 404,
|
|
209
|
+
}],
|
|
210
|
+
chainingKeys: [],
|
|
211
|
+
requiresAuth: false,
|
|
212
|
+
estimatedComplexity: "simple",
|
|
213
|
+
source: ScenarioSource.CodeInferred,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
194
216
|
return capScenarios(scenarios);
|
|
195
217
|
}
|
|
196
218
|
const MAX_TOTAL_SCENARIOS = 30;
|
package/build/utils/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
export declare function readDiffFile(diffFilePath: string | undefined): string | undefined;
|
|
3
3
|
export declare function toolError(message: string): CallToolResult;
|
|
4
4
|
/**
|
|
@@ -7,6 +7,12 @@ export declare function toolError(message: string): CallToolResult;
|
|
|
7
7
|
* `{ content: [{ type: "text", text }] }` boilerplate themselves.
|
|
8
8
|
*/
|
|
9
9
|
export declare function toolText(text: string): CallToolResult;
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a single text payload in the MCP resource-result shape so resource
|
|
12
|
+
* handlers don't have to construct
|
|
13
|
+
* `{ contents: [{ uri, mimeType, text }] }` boilerplate themselves.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resourceText(uri: string, text: string, mimeType?: string): ReadResourceResult;
|
|
10
16
|
export declare const OUTPUT_DIR_FIELD_NAME = "outputDir";
|
|
11
17
|
export declare const TRACE_OUTPUT_FILE_FIELD_NAME = "traceOutputFile";
|
|
12
18
|
export declare const PLAYWRIGHT_OUTPUT_FILE_FIELD_NAME = "playwrightOutput";
|
package/build/utils/utils.js
CHANGED
|
@@ -28,6 +28,16 @@ export function toolText(text) {
|
|
|
28
28
|
content: [{ type: "text", text }],
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Wraps a single text payload in the MCP resource-result shape so resource
|
|
33
|
+
* handlers don't have to construct
|
|
34
|
+
* `{ contents: [{ uri, mimeType, text }] }` boilerplate themselves.
|
|
35
|
+
*/
|
|
36
|
+
export function resourceText(uri, text, mimeType = "text/plain") {
|
|
37
|
+
return {
|
|
38
|
+
contents: [{ uri, mimeType, text }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
31
41
|
export const OUTPUT_DIR_FIELD_NAME = "outputDir";
|
|
32
42
|
export const TRACE_OUTPUT_FILE_FIELD_NAME = "traceOutputFile";
|
|
33
43
|
export const PLAYWRIGHT_OUTPUT_FILE_FIELD_NAME = "playwrightOutput";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const SKYRAMP_IMAGE_VERSION = "v1.3.
|
|
2
|
-
export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.
|
|
3
|
-
export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.
|
|
1
|
+
export declare const SKYRAMP_IMAGE_VERSION = "v1.3.29";
|
|
2
|
+
export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.29";
|
|
3
|
+
export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.29";
|
package/build/utils/versions.js
CHANGED
|
@@ -3,7 +3,7 @@ export declare const WORKSPACE_DIR = ".skyramp";
|
|
|
3
3
|
export declare const WORKSPACE_FILENAME = "workspace.yml";
|
|
4
4
|
export declare const serviceSchema: z.ZodObject<{
|
|
5
5
|
serviceName: z.ZodString;
|
|
6
|
-
|
|
6
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
7
7
|
language: z.ZodOptional<z.ZodEnum<["python", "typescript", "javascript", "java"]>>;
|
|
8
8
|
framework: z.ZodOptional<z.ZodEnum<["playwright", "pytest", "robot", "junit"]>>;
|
|
9
9
|
testDirectory: z.ZodOptional<z.ZodString>;
|
|
@@ -49,7 +49,7 @@ export declare const serviceSchema: z.ZodObject<{
|
|
|
49
49
|
serviceName: string;
|
|
50
50
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
51
51
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
52
|
-
|
|
52
|
+
repository?: string | undefined;
|
|
53
53
|
testDirectory?: string | undefined;
|
|
54
54
|
api?: {
|
|
55
55
|
authHeader?: string | undefined;
|
|
@@ -69,7 +69,7 @@ export declare const serviceSchema: z.ZodObject<{
|
|
|
69
69
|
serviceName: string;
|
|
70
70
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
71
71
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
72
|
-
|
|
72
|
+
repository?: string | undefined;
|
|
73
73
|
testDirectory?: string | undefined;
|
|
74
74
|
api?: {
|
|
75
75
|
authHeader?: string | undefined;
|
|
@@ -118,7 +118,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
|
|
|
118
118
|
}>>;
|
|
119
119
|
services: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
120
120
|
serviceName: z.ZodString;
|
|
121
|
-
|
|
121
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
122
122
|
language: z.ZodOptional<z.ZodEnum<["python", "typescript", "javascript", "java"]>>;
|
|
123
123
|
framework: z.ZodOptional<z.ZodEnum<["playwright", "pytest", "robot", "junit"]>>;
|
|
124
124
|
testDirectory: z.ZodOptional<z.ZodString>;
|
|
@@ -164,7 +164,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
|
|
|
164
164
|
serviceName: string;
|
|
165
165
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
166
166
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
167
|
-
|
|
167
|
+
repository?: string | undefined;
|
|
168
168
|
testDirectory?: string | undefined;
|
|
169
169
|
api?: {
|
|
170
170
|
authHeader?: string | undefined;
|
|
@@ -184,7 +184,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
|
|
|
184
184
|
serviceName: string;
|
|
185
185
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
186
186
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
187
|
-
|
|
187
|
+
repository?: string | undefined;
|
|
188
188
|
testDirectory?: string | undefined;
|
|
189
189
|
api?: {
|
|
190
190
|
authHeader?: string | undefined;
|
|
@@ -217,7 +217,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
|
|
|
217
217
|
serviceName: string;
|
|
218
218
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
219
219
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
220
|
-
|
|
220
|
+
repository?: string | undefined;
|
|
221
221
|
testDirectory?: string | undefined;
|
|
222
222
|
api?: {
|
|
223
223
|
authHeader?: string | undefined;
|
|
@@ -250,7 +250,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
|
|
|
250
250
|
serviceName: string;
|
|
251
251
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
252
252
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
253
|
-
|
|
253
|
+
repository?: string | undefined;
|
|
254
254
|
testDirectory?: string | undefined;
|
|
255
255
|
api?: {
|
|
256
256
|
authHeader?: string | undefined;
|
|
@@ -291,7 +291,7 @@ export declare function validateWorkspaceConfig(config: unknown): z.SafeParseRet
|
|
|
291
291
|
serviceName: string;
|
|
292
292
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
293
293
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
294
|
-
|
|
294
|
+
repository?: string | undefined;
|
|
295
295
|
testDirectory?: string | undefined;
|
|
296
296
|
api?: {
|
|
297
297
|
authHeader?: string | undefined;
|
|
@@ -324,7 +324,7 @@ export declare function validateWorkspaceConfig(config: unknown): z.SafeParseRet
|
|
|
324
324
|
serviceName: string;
|
|
325
325
|
language?: "python" | "typescript" | "javascript" | "java" | undefined;
|
|
326
326
|
framework?: "playwright" | "pytest" | "robot" | "junit" | undefined;
|
|
327
|
-
|
|
327
|
+
repository?: string | undefined;
|
|
328
328
|
testDirectory?: string | undefined;
|
|
329
329
|
api?: {
|
|
330
330
|
authHeader?: string | undefined;
|
|
@@ -27,9 +27,9 @@ export const serviceSchema = z
|
|
|
27
27
|
.object({
|
|
28
28
|
serviceName: z.string(),
|
|
29
29
|
// owner/repo this service belongs to (multi-repo workspaces). Absent → the
|
|
30
|
-
// primary repo. Service identity is the composite (
|
|
31
|
-
// same serviceName can appear once per repo without colliding.
|
|
32
|
-
|
|
30
|
+
// primary repo. Service identity is the composite (repository, serviceName),
|
|
31
|
+
// so the same serviceName can appear once per repo without colliding.
|
|
32
|
+
repository: z.string().optional(),
|
|
33
33
|
language: z.enum(["python", "typescript", "javascript", "java"]).optional(),
|
|
34
34
|
framework: z.enum(["playwright", "pytest", "robot", "junit"]).optional(),
|
|
35
35
|
testDirectory: z.string().optional(),
|
|
@@ -318,11 +318,11 @@ export class WorkspaceConfigManager {
|
|
|
318
318
|
const validatedService = svcResult.data;
|
|
319
319
|
const config = await this.read();
|
|
320
320
|
const services = config.services || [];
|
|
321
|
-
// Upsert keyed on the composite (
|
|
322
|
-
// to "" (primary) so a primary service and a same-named
|
|
321
|
+
// Upsert keyed on the composite (repository, serviceName). Absent repository
|
|
322
|
+
// normalizes to "" (primary) so a primary service and a same-named related-repo
|
|
323
323
|
// service are distinct entries.
|
|
324
|
-
const repoKey = validatedService.
|
|
325
|
-
const idx = services.findIndex((s) => (s.
|
|
324
|
+
const repoKey = validatedService.repository || "";
|
|
325
|
+
const idx = services.findIndex((s) => (s.repository || "") === repoKey &&
|
|
326
326
|
s.serviceName === validatedService.serviceName);
|
|
327
327
|
if (idx >= 0) {
|
|
328
328
|
services[idx] = validatedService;
|
|
@@ -6,10 +6,10 @@ import yaml from "js-yaml";
|
|
|
6
6
|
import { serviceSchema, WorkspaceConfigManager, validateWorkspaceConfig, createDefaultConfig, WORKSPACE_DIR, WORKSPACE_FILENAME, } from "./workspace.js";
|
|
7
7
|
describe("workspace module (ported from @skyramp/skyramp, SKYR-3794)", () => {
|
|
8
8
|
describe("serviceSchema", () => {
|
|
9
|
-
it("accepts an optional
|
|
10
|
-
expect(serviceSchema.safeParse({ serviceName: "api",
|
|
9
|
+
it("accepts an optional repository and rejects a non-string repository", () => {
|
|
10
|
+
expect(serviceSchema.safeParse({ serviceName: "api", repository: "org/api" }).success).toBe(true);
|
|
11
11
|
expect(serviceSchema.safeParse({ serviceName: "api" }).success).toBe(true);
|
|
12
|
-
expect(serviceSchema.safeParse({ serviceName: "api",
|
|
12
|
+
expect(serviceSchema.safeParse({ serviceName: "api", repository: 123 }).success).toBe(false);
|
|
13
13
|
});
|
|
14
14
|
it("rejects unknown fields (strict)", () => {
|
|
15
15
|
expect(serviceSchema.safeParse({ serviceName: "api", bogus: true }).success).toBe(false);
|
|
@@ -42,12 +42,12 @@ describe("workspace module (ported from @skyramp/skyramp, SKYR-3794)", () => {
|
|
|
42
42
|
const mgr = new WorkspaceConfigManager(repoDir);
|
|
43
43
|
await mgr.initialize();
|
|
44
44
|
await mgr.addService({ serviceName: "svc", testDirectory: "tests/primary" });
|
|
45
|
-
await mgr.addService({ serviceName: "svc",
|
|
45
|
+
await mgr.addService({ serviceName: "svc", repository: "letsramp/api-insight", testDirectory: "tests/py" });
|
|
46
46
|
await mgr.addService({ serviceName: "svc", testDirectory: "tests/primary-v2" }); // upsert primary
|
|
47
47
|
const cfg = read();
|
|
48
48
|
expect(cfg.services).toHaveLength(2);
|
|
49
|
-
expect(cfg.services.find((s) => !s.
|
|
50
|
-
expect(cfg.services.find((s) => s.
|
|
49
|
+
expect(cfg.services.find((s) => !s.repository).testDirectory).toBe("tests/primary-v2");
|
|
50
|
+
expect(cfg.services.find((s) => s.repository === "letsramp/api-insight").testDirectory).toBe("tests/py");
|
|
51
51
|
// repoName auto-detected from the git remote.
|
|
52
52
|
expect(cfg.workspace.repoName).toBe("letsramp/primary");
|
|
53
53
|
});
|
|
@@ -36,20 +36,23 @@ const loadTraceSchema = {
|
|
|
36
36
|
name: "skyramp_load_trace",
|
|
37
37
|
title: "Load and replay a Skyramp trace",
|
|
38
38
|
description: [
|
|
39
|
-
"Load a previously recorded Skyramp trace (.zip or .jsonl/.txt) and replay its actions against the live browser to restore state, then continue recording from that point.",
|
|
40
|
-
"The replayed actions are merged into the current recording, so subsequent browser_* steps append to them and skyramp_export_zip writes a combined trace.",
|
|
41
|
-
"
|
|
42
|
-
"-
|
|
39
|
+
"Load a previously recorded Skyramp trace (.zip or .jsonl/.txt) and replay a range of its actions against the live browser to restore state, then continue recording from that point.",
|
|
40
|
+
"The replayed actions are merged into the current recording, so subsequent browser_* steps append to them and skyramp_export_zip writes a combined trace. You can record some steps FIRST, then load a trace to insert an already-recorded fragment at the current point.",
|
|
41
|
+
"STARTING LATER (skip a stale prefix): set startAtStep to begin replay partway in, skipping earlier steps. Use this when the live browser is already past the trace's opening steps - e.g. the login page changed, you recorded the NEW login manually, and now want to append only the still-valid post-login part of the old trace. Omit to start from step 1.",
|
|
42
|
+
"- startAtStep: begin replay at step N (1-based, from the numbered step list); steps 1..N-1 are skipped (neither replayed nor recorded).",
|
|
43
|
+
"STOPPING EARLY: when the user wants to stop partway through the loaded trace before continuing, set ONE of stopAtStep / stopAtUrl / stopBefore based on their prompt. Omit all three to replay to the end.",
|
|
44
|
+
"- stopAtStep: replay up to and including step N then stop (N is 1-based, from the numbered step list).",
|
|
43
45
|
"- stopAtUrl: stop once the active page URL matches this glob/regex/substring (checked after each action).",
|
|
44
46
|
'- stopBefore: stop right BEFORE the first action whose description contains this text (case-insensitive), e.g. "Checkout" to stop before clicking Checkout.',
|
|
45
|
-
"If unsure where to stop, first call with dryRun:true to read the numbered step list, then call again with the chosen stop
|
|
47
|
+
"If unsure where to start/stop, first call with dryRun:true to read the numbered step list, then call again with the chosen start/stop parameters."
|
|
46
48
|
].join(" "),
|
|
47
49
|
inputSchema: import_mcpBundle.z.object({
|
|
48
50
|
path: import_mcpBundle.z.string().describe("Absolute path to the trace file (.zip, .jsonl, or .txt)."),
|
|
49
|
-
|
|
51
|
+
startAtStep: import_mcpBundle.z.number().int().positive().optional().describe("Begin replay at step N (1-based); skip steps 1..N-1. Use to skip a stale prefix (e.g. old login)."),
|
|
52
|
+
stopAtStep: import_mcpBundle.z.number().int().positive().optional().describe("Replay up to and including step N (1-based), then stop before continuing."),
|
|
50
53
|
stopAtUrl: import_mcpBundle.z.string().optional().describe("Stop once the active page URL matches this glob/regex/substring."),
|
|
51
54
|
stopBefore: import_mcpBundle.z.string().optional().describe("Stop before the first action whose description contains this text (case-insensitive)."),
|
|
52
|
-
dryRun: import_mcpBundle.z.boolean().optional().describe("Parse and list the steps WITHOUT replaying. Use to choose
|
|
55
|
+
dryRun: import_mcpBundle.z.boolean().optional().describe("Parse and list the steps WITHOUT replaying. Use to choose start/stop points."),
|
|
53
56
|
speed: import_mcpBundle.z.enum(["fast", "slow"]).optional().describe('Replay speed. "slow" adds a 1s delay between actions (default "fast").')
|
|
54
57
|
}),
|
|
55
58
|
// Replaying drives the live browser and mutates the recording, so this is an
|
|
@@ -134,17 +137,30 @@ function describeStopReason(result) {
|
|
|
134
137
|
async function replayActions(actionsList, callbacks, params) {
|
|
135
138
|
const delay = params.speed === "slow" ? 1e3 : 0;
|
|
136
139
|
const stopBefore = params.stopBefore?.toLowerCase();
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
const startIndex = params.startAtStep ? Math.max(0, Math.min(params.startAtStep - 1, actionsList.length)) : 0;
|
|
141
|
+
let completedCount = startIndex;
|
|
142
|
+
for (let i = startIndex; i < actionsList.length; i++) {
|
|
139
143
|
const action = actionsList[i];
|
|
140
144
|
if (stopBefore && describeStep(action, i).toLowerCase().includes(stopBefore))
|
|
141
|
-
return { completedCount, stopIndex: i, stopReason: "stopBefore" };
|
|
145
|
+
return { completedCount, startIndex, stopIndex: i, stopReason: "stopBefore" };
|
|
146
|
+
try {
|
|
147
|
+
callbacks.beforeAction?.(action);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return {
|
|
150
|
+
completedCount,
|
|
151
|
+
startIndex,
|
|
152
|
+
stopIndex: i,
|
|
153
|
+
stopReason: "error",
|
|
154
|
+
error: { message: e.message, actionName: action.action.name, stepIndex: i + 1 }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
142
157
|
const alias = action.frame.pageAlias || "page";
|
|
143
158
|
const page = callbacks.pageForAlias(alias);
|
|
144
159
|
if (!METADATA_ONLY_ACTIONS.has(action.action.name)) {
|
|
145
160
|
if (!page) {
|
|
146
161
|
return {
|
|
147
162
|
completedCount,
|
|
163
|
+
startIndex,
|
|
148
164
|
stopIndex: i,
|
|
149
165
|
stopReason: "error",
|
|
150
166
|
error: { message: `No live page for alias "${alias}"`, actionName: action.action.name, stepIndex: i + 1 }
|
|
@@ -152,9 +168,11 @@ async function replayActions(actionsList, callbacks, params) {
|
|
|
152
168
|
}
|
|
153
169
|
try {
|
|
154
170
|
await performClientAction(page, action);
|
|
171
|
+
await callbacks.afterAction?.(action);
|
|
155
172
|
} catch (e) {
|
|
156
173
|
return {
|
|
157
174
|
completedCount,
|
|
175
|
+
startIndex,
|
|
158
176
|
stopIndex: i,
|
|
159
177
|
stopReason: "error",
|
|
160
178
|
error: { message: e.message, actionName: action.action.name, stepIndex: i + 1 }
|
|
@@ -163,16 +181,16 @@ async function replayActions(actionsList, callbacks, params) {
|
|
|
163
181
|
}
|
|
164
182
|
completedCount = i + 1;
|
|
165
183
|
if (params.stopAtStep !== void 0 && completedCount >= params.stopAtStep)
|
|
166
|
-
return { completedCount, stopIndex: i + 1, stopReason: "stopAtStep" };
|
|
184
|
+
return { completedCount, startIndex, stopIndex: i + 1, stopReason: "stopAtStep" };
|
|
167
185
|
if (params.stopAtUrl) {
|
|
168
186
|
const currentUrl = page?.url() ?? "";
|
|
169
187
|
if (urlMatchesPattern(currentUrl, params.stopAtUrl))
|
|
170
|
-
return { completedCount, stopIndex: i + 1, stopReason: "stopAtUrl" };
|
|
188
|
+
return { completedCount, startIndex, stopIndex: i + 1, stopReason: "stopAtUrl" };
|
|
171
189
|
}
|
|
172
190
|
if (delay)
|
|
173
191
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
174
192
|
}
|
|
175
|
-
return { completedCount, stopIndex: actionsList.length, stopReason: "done" };
|
|
193
|
+
return { completedCount, startIndex, stopIndex: actionsList.length, stopReason: "done" };
|
|
176
194
|
}
|
|
177
195
|
function urlMatchesPattern(currentUrl, pattern) {
|
|
178
196
|
const regexLiteral = /^\/(.*)\/([a-z]*)$/i.exec(pattern);
|
|
@@ -111,6 +111,11 @@ class TraceRecordingBackend {
|
|
|
111
111
|
},
|
|
112
112
|
contextOptions: {
|
|
113
113
|
viewport: import_skyRampExport.RECORDING_VIEWPORT,
|
|
114
|
+
// Mirror `playwright codegen --load-storage`: seed the recording
|
|
115
|
+
// browser with a saved session so it records against an authenticated
|
|
116
|
+
// app. Same source (PLAYWRIGHT_MCP_STORAGE_STATE) that buildJsonlContent
|
|
117
|
+
// stamps into the export header, so load + emit stay consistent.
|
|
118
|
+
...process.env.PLAYWRIGHT_MCP_STORAGE_STATE?.trim() ? { storageState: process.env.PLAYWRIGHT_MCP_STORAGE_STATE.trim() } : {},
|
|
114
119
|
recordHar: { path: this._harPath, mode: "minimal" },
|
|
115
120
|
ignoreHTTPSErrors: true,
|
|
116
121
|
serviceWorkers: "block"
|
|
@@ -520,6 +525,9 @@ Failed to load trace from ${params.path}: ${e.message}` }], isError: true };
|
|
|
520
525
|
if (!loadedActions.length)
|
|
521
526
|
return { content: [{ type: "text", text: `### Error
|
|
522
527
|
No actions found in ${params.path}.` }], isError: true };
|
|
528
|
+
if (params.startAtStep !== void 0 && params.stopAtStep !== void 0 && params.startAtStep > params.stopAtStep)
|
|
529
|
+
return { content: [{ type: "text", text: `### Error
|
|
530
|
+
startAtStep (${params.startAtStep}) is after stopAtStep (${params.stopAtStep}).` }], isError: true };
|
|
523
531
|
if (params.dryRun) {
|
|
524
532
|
const stepList = loadedActions.map((a, i) => (0, import_loadTraceTool.describeStep)(a, i)).join("\n");
|
|
525
533
|
return {
|
|
@@ -531,7 +539,7 @@ ${loadedActions.length} steps in ${params.path}. Nothing was replayed.
|
|
|
531
539
|
Steps:
|
|
532
540
|
${stepList}
|
|
533
541
|
|
|
534
|
-
Call skyramp_load_trace again
|
|
542
|
+
Call skyramp_load_trace again to replay: set startAtStep to skip a stale prefix (e.g. an old login) and/or one of stopAtStep / stopAtUrl / stopBefore to stop early. Omit all to replay the whole trace.`
|
|
535
543
|
}]
|
|
536
544
|
};
|
|
537
545
|
}
|
|
@@ -539,17 +547,83 @@ Call skyramp_load_trace again with stopAtStep / stopAtUrl / stopBefore (or none
|
|
|
539
547
|
await this._browserBackend.context.ensureTab();
|
|
540
548
|
} catch {
|
|
541
549
|
}
|
|
550
|
+
const startIndex = params.startAtStep ? Math.max(0, Math.min(params.startAtStep - 1, loadedActions.length)) : 0;
|
|
551
|
+
let guardEnd = loadedActions.length;
|
|
552
|
+
if (params.stopAtStep) {
|
|
553
|
+
guardEnd = Math.min(guardEnd, params.stopAtStep);
|
|
554
|
+
} else if (params.stopBefore) {
|
|
555
|
+
const needle = params.stopBefore.toLowerCase();
|
|
556
|
+
const matchIndex = loadedActions.findIndex((a, i) => i >= startIndex && (0, import_loadTraceTool.describeStep)(a, i).toLowerCase().includes(needle));
|
|
557
|
+
if (matchIndex !== -1)
|
|
558
|
+
guardEnd = matchIndex;
|
|
559
|
+
}
|
|
560
|
+
const replayRange = loadedActions.slice(startIndex, guardEnd);
|
|
561
|
+
const primaryAlias = replayRange[0]?.frame.pageAlias || "page";
|
|
562
|
+
{
|
|
563
|
+
const available = /* @__PURE__ */ new Set([primaryAlias]);
|
|
564
|
+
for (const a of replayRange) {
|
|
565
|
+
if (!available.has(a.frame.pageAlias))
|
|
566
|
+
return { content: [{ type: "text", text: `### Error
|
|
567
|
+
The selected range acts on tab "${a.frame.pageAlias}" but the step that opens that tab is outside the range (skipped by startAtStep). Lower startAtStep to include the opening step, or pick a range that stays on a single tab.` }], isError: true };
|
|
568
|
+
for (const s of a.action.signals) {
|
|
569
|
+
if (s.name === "popup")
|
|
570
|
+
available.add(s.popupAlias);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const liveTabs = () => (this._browserBackend.context?.tabs() ?? []).map((t) => t.page);
|
|
575
|
+
const seedPage = this._browserBackend.context?.currentTab()?.page ?? liveTabs()[0];
|
|
576
|
+
const remap = /* @__PURE__ */ new Map();
|
|
577
|
+
remap.set(primaryAlias, { page: seedPage, liveAlias: this._currentPageAlias });
|
|
578
|
+
const claimedTabs = /* @__PURE__ */ new Set();
|
|
579
|
+
if (seedPage)
|
|
580
|
+
claimedTabs.add(seedPage);
|
|
581
|
+
let tabsBefore = [];
|
|
542
582
|
let result;
|
|
543
583
|
try {
|
|
544
|
-
result = await (0, import_loadTraceTool.replayActions)(loadedActions, {
|
|
584
|
+
result = await (0, import_loadTraceTool.replayActions)(loadedActions, {
|
|
585
|
+
pageForAlias: (alias) => remap.get(alias)?.page ?? this._pageForAlias(alias),
|
|
586
|
+
beforeAction: () => {
|
|
587
|
+
tabsBefore = liveTabs();
|
|
588
|
+
},
|
|
589
|
+
afterAction: async (action) => {
|
|
590
|
+
for (const signal of action.action.signals) {
|
|
591
|
+
if (signal.name !== "popup")
|
|
592
|
+
continue;
|
|
593
|
+
let fresh;
|
|
594
|
+
for (let k = 0; k < 100 && !fresh; k++) {
|
|
595
|
+
fresh = liveTabs().find((p) => !tabsBefore.includes(p) && !claimedTabs.has(p));
|
|
596
|
+
if (!fresh)
|
|
597
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
598
|
+
}
|
|
599
|
+
if (!fresh) {
|
|
600
|
+
throw new Error(`Expected a popup ("${signal.popupAlias}") to open after this action, but no new tab appeared \u2014 the fragment has drifted from the live page.`);
|
|
601
|
+
}
|
|
602
|
+
claimedTabs.add(fresh);
|
|
603
|
+
remap.set(signal.popupAlias, { page: fresh, liveAlias: this._currentPageAlias });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}, params);
|
|
545
607
|
} catch (e) {
|
|
546
608
|
return { content: [{ type: "text", text: `### Error
|
|
547
609
|
Replay failed: ${e.message}` }], isError: true };
|
|
548
610
|
}
|
|
549
|
-
for (let i =
|
|
550
|
-
|
|
551
|
-
|
|
611
|
+
for (let i = result.startIndex; i < result.stopIndex; i++) {
|
|
612
|
+
const a = loadedActions[i];
|
|
613
|
+
const liveAlias = remap.get(a.frame.pageAlias)?.liveAlias;
|
|
614
|
+
if (liveAlias && liveAlias !== a.frame.pageAlias)
|
|
615
|
+
a.frame = { ...a.frame, pageAlias: liveAlias };
|
|
616
|
+
const before = this._trackedActions.length;
|
|
617
|
+
this._seedTrackedAction(a);
|
|
618
|
+
const popupSignal = a.action.signals.find((s) => s.name === "popup");
|
|
619
|
+
if (popupSignal && this._trackedActions.length > before)
|
|
620
|
+
this._trackedActions[this._trackedActions.length - 1].popupAlias = remap.get(popupSignal.popupAlias)?.liveAlias ?? popupSignal.popupAlias;
|
|
621
|
+
}
|
|
622
|
+
if (result.stopIndex > result.startIndex)
|
|
552
623
|
this._currentPageAlias = loadedActions[result.stopIndex - 1].frame.pageAlias || "page";
|
|
624
|
+
const replayedCount = Math.max(0, result.stopIndex - result.startIndex);
|
|
625
|
+
const clamped = params.startAtStep !== void 0 && params.startAtStep !== result.startIndex + 1;
|
|
626
|
+
const skippedNote = result.startIndex > 0 ? ` Skipped the first ${result.startIndex} step${result.startIndex === 1 ? "" : "s"} (startAtStep ${params.startAtStep}${clamped ? `, clamped \u2014 trace has ${loadedActions.length} steps` : ""}).` : "";
|
|
553
627
|
const remaining = (0, import_loadTraceTool.listStepsFrom)(loadedActions, result.stopIndex);
|
|
554
628
|
const stoppedNote = (0, import_loadTraceTool.describeStopReason)(result);
|
|
555
629
|
const errorNote = result.error ? `
|
|
@@ -560,7 +634,7 @@ You can continue recording from this point or fix the issue.` : "";
|
|
|
560
634
|
content: [{
|
|
561
635
|
type: "text",
|
|
562
636
|
text: `### Trace replayed
|
|
563
|
-
${
|
|
637
|
+
${replayedCount} of ${loadedActions.length} steps replayed from ${params.path}.${skippedNote} ${stoppedNote}${errorNote}
|
|
564
638
|
|
|
565
639
|
Remaining steps (NOT replayed):
|
|
566
640
|
${remaining}
|
|
@@ -1658,10 +1732,9 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
|
|
|
1658
1732
|
const url = page.url();
|
|
1659
1733
|
if (url.startsWith("chrome-extension://") || url.startsWith("about:") || url.startsWith("file://"))
|
|
1660
1734
|
return;
|
|
1661
|
-
const alreadyEnabled = await page.evaluate(
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
}).catch(() => false);
|
|
1735
|
+
const alreadyEnabled = await page.evaluate(
|
|
1736
|
+
() => document.querySelector("flt-semantics-placeholder") === null && document.querySelector("flt-semantics-host") !== null
|
|
1737
|
+
).catch(() => false);
|
|
1665
1738
|
if (alreadyEnabled) {
|
|
1666
1739
|
traceDebug("Flutter accessibility already enabled");
|
|
1667
1740
|
return;
|
|
@@ -1689,14 +1762,14 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
|
|
|
1689
1762
|
traceDebug("Flutter detected but placeholder click failed");
|
|
1690
1763
|
return;
|
|
1691
1764
|
}
|
|
1692
|
-
const enabled = await page.waitForFunction(
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1765
|
+
const enabled = await page.waitForFunction(
|
|
1766
|
+
() => document.querySelector("flt-semantics-placeholder") === null,
|
|
1767
|
+
{ timeout: 1e4 }
|
|
1768
|
+
).then(() => true).catch(() => false);
|
|
1696
1769
|
if (enabled)
|
|
1697
1770
|
traceDebug("Flutter accessibility mode enabled");
|
|
1698
1771
|
else
|
|
1699
|
-
traceDebug("Flutter placeholder clicked but semantics
|
|
1772
|
+
traceDebug("Flutter placeholder clicked but semantics not confirmed active");
|
|
1700
1773
|
}
|
|
1701
1774
|
/** Clean up temp directory and optionally recreate it for next session. */
|
|
1702
1775
|
_cleanupTempDir(recreate = false) {
|
|
@@ -55,12 +55,13 @@ function modifiersArrayToMask(modifiers) {
|
|
|
55
55
|
return mask;
|
|
56
56
|
}
|
|
57
57
|
const RECORDING_VIEWPORT = { width: 1280, height: 900 };
|
|
58
|
-
function jsonlHeader(browserName, harPath, viewport) {
|
|
58
|
+
function jsonlHeader(browserName, harPath, viewport, storageState) {
|
|
59
59
|
return JSON.stringify({
|
|
60
60
|
browserName,
|
|
61
61
|
launchOptions: { headless: false },
|
|
62
62
|
contextOptions: {
|
|
63
63
|
viewport,
|
|
64
|
+
...storageState ? { storageState } : {},
|
|
64
65
|
ignoreHTTPSErrors: true,
|
|
65
66
|
recordHar: { path: harPath, mode: "minimal" },
|
|
66
67
|
serviceWorkers: "block"
|
|
@@ -623,7 +624,7 @@ function mouseActionToJsonl(action, pageGuid, baseTs, alias) {
|
|
|
623
624
|
return [];
|
|
624
625
|
}
|
|
625
626
|
}
|
|
626
|
-
function buildJsonlContent(actions, browserName, harPath, viewport = RECORDING_VIEWPORT) {
|
|
627
|
+
function buildJsonlContent(actions, browserName, harPath, viewport = RECORDING_VIEWPORT, storageState = process.env.PLAYWRIGHT_MCP_STORAGE_STATE?.trim() || void 0) {
|
|
627
628
|
normalizePrimaryPageAlias(actions);
|
|
628
629
|
const deduplicated = removeRedundantClicks(deduplicateRetries(actions));
|
|
629
630
|
const startTime = deduplicated[0]?.timestamp ?? Date.now();
|
|
@@ -635,7 +636,7 @@ function buildJsonlContent(actions, browserName, harPath, viewport = RECORDING_V
|
|
|
635
636
|
};
|
|
636
637
|
const mainGuid = getPageGuid(DEFAULT_PAGE_ALIAS);
|
|
637
638
|
const lines = [
|
|
638
|
-
jsonlHeader(browserName, harPath, viewport),
|
|
639
|
+
jsonlHeader(browserName, harPath, viewport, storageState),
|
|
639
640
|
JSON.stringify({ name: "openPage", url: "about:blank", signals: [], timestamp: String(startTime - 1e3), pageGuid: mainGuid, pageAlias: DEFAULT_PAGE_ALIAS, framePath: DEFAULT_FRAME_PATH })
|
|
640
641
|
];
|
|
641
642
|
let actionCount = 0;
|