@skyramp/mcp 0.4.4 → 0.5.0-rc.10
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/entitlements/entitlementGate.d.ts +8 -0
- package/build/entitlements/entitlementGate.js +57 -0
- package/build/entitlements/runCodes.d.ts +18 -0
- package/build/entitlements/runCodes.js +16 -0
- package/build/index.js +32 -6
- package/build/playwright/PlaywrightTraceService.d.ts +17 -0
- package/build/playwright/PlaywrightTraceService.js +29 -1
- package/build/playwright/dataSafetyConfig.d.ts +74 -0
- package/build/playwright/dataSafetyConfig.js +109 -0
- package/build/playwright/essentialTools.d.ts +1 -0
- package/build/playwright/essentialTools.js +44 -0
- package/build/playwright/registerPlaywrightTools.d.ts +36 -1
- package/build/playwright/registerPlaywrightTools.js +87 -43
- package/build/playwright/safeResolveDataSafety.d.ts +24 -0
- package/build/playwright/safeResolveDataSafety.js +33 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +46 -177
- package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/test-maintenance/driftAnalysisShared.js +1 -1
- package/build/services/FleetExecutionService.d.ts +87 -16
- package/build/services/FleetExecutionService.js +487 -48
- package/build/services/FleetRunRegistry.d.ts +49 -0
- package/build/services/FleetRunRegistry.js +178 -0
- package/build/services/containerEnv.js +8 -2
- package/build/services/fleetLimitConfirmation.d.ts +19 -0
- package/build/services/fleetLimitConfirmation.js +126 -0
- package/build/tool-phases.d.ts +14 -8
- package/build/tool-phases.js +88 -43
- package/build/tools/code-refactor/assertion-state.js +3 -1
- package/build/tools/code-refactor/codeReuseTool.js +2 -1
- package/build/tools/code-refactor/utils-verify-gates.js +1 -1
- package/build/tools/executeFleetTool.d.ts +6 -4
- package/build/tools/executeFleetTool.js +67 -23
- package/build/tools/fixErrorTool.js +2 -1
- package/build/tools/fleetLifecycleTools.d.ts +20 -0
- package/build/tools/fleetLifecycleTools.js +124 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +38 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.js +7 -1
- package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
- package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
- package/build/tools/submitReportTool.js +4 -122
- package/build/tools/test-management/registerTestPlanTool.js +22 -6
- package/build/tools/workspace/initScanWorkspaceTool.d.ts +11 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +111 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +40 -4
- package/build/types/AssertionOutcome.d.ts +3 -0
- package/build/types/TestbotReport.d.ts +0 -37
- package/build/types/TestbotReport.js +0 -22
- package/build/types/index.d.ts +1 -1
- package/build/types/index.js +1 -1
- package/build/utils/assertion-verify/created-add-guard.d.ts +12 -0
- package/build/utils/assertion-verify/created-add-guard.js +173 -0
- package/build/utils/assertion-verify/index.d.ts +1 -0
- package/build/utils/assertion-verify/index.js +1 -0
- package/build/utils/assertion-verify/verify.d.ts +7 -3
- package/build/utils/assertion-verify/verify.js +9 -3
- package/build/utils/featureFlags.js +6 -3
- package/build/utils/repositorySlug.d.ts +14 -0
- package/build/utils/repositorySlug.js +46 -1
- package/build/utils/telemetry.d.ts +2 -0
- package/build/utils/telemetry.js +12 -0
- package/build/utils/utils-verify/action-key.d.ts +59 -2
- package/build/utils/utils-verify/action-key.js +162 -6
- package/build/utils/utils-verify/action-sites.d.ts +7 -0
- package/build/utils/utils-verify/action-sites.js +277 -35
- package/build/utils/utils-verify/call-sites.d.ts +8 -2
- package/build/utils/utils-verify/language-spec.js +1 -1
- package/build/utils/utils-verify/parse.d.ts +4 -0
- package/build/utils/utils-verify/parse.js +2 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +63 -121
- package/build/workspace/workspace.js +45 -48
- package/build/workspace/workspaceVerifier.d.ts +22 -0
- package/build/workspace/workspaceVerifier.js +65 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +7 -6
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +5 -4
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +12 -0
- package/node_modules/playwright/lib/mcp/skyramp/dataSafetyGuard.js +822 -0
- package/node_modules/playwright/lib/mcp/skyramp/dataSafetySession.js +372 -0
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +101 -8
- package/node_modules/playwright/lib/mcp/skyramp/finishTestSetupTool.js +45 -0
- package/node_modules/playwright/lib/mcp/skyramp/index.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/recordingPhase.js +65 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +1 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +598 -127
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +85 -31
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +132 -5
- package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/client/browserContext.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/protocol/validator.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/browserContext.js +17 -1
- 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/har/harRecorder.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harTracer.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/clickExpandedState.js +75 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +85 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bou8kKzE.js → codeMirrorModule-BuhAdckO.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-COAHN430.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.0MJW0uG7.js → index.7AU8h0PZ.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.GLqsx5cI.js → uiMode.Dl7smBJn.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/cli/program.ts +5 -0
- package/node_modules/playwright/node_modules/playwright-core/src/client/browserContext.ts +3 -1
- package/node_modules/playwright/node_modules/playwright-core/src/client/types.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/protocol/validator.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/browserContext.ts +25 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/har/harRecorder.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/har/harTracer.ts +3 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +3 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/clickExpandedState.ts +82 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +2 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +137 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -4
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/prompts/testbot-task1.md +1 -1
- package/plugin/skills/enhance-assertions/reference/integration.md +1 -0
- package/plugin/skills/setup-testbot-target-sut/SKILL.md +60 -0
- package/plugin/skills/setup-testbot-target-sut/reference/ci-workflow.md +26 -0
- package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +105 -0
- package/plugin/skills/setup-testbot-target-sut/reference/existing-tests.md +14 -0
- package/plugin/skills/setup-testbot-target-sut/reference/troubleshooting.md +20 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +0 -2
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +0 -178
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +0 -2
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +0 -311
- package/build/prompts/sut-setup/shared.d.ts +0 -62
- package/build/prompts/sut-setup/shared.js +0 -196
- package/build/resources/sutSetupResource.d.ts +0 -2
- package/build/resources/sutSetupResource.js +0 -63
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DTp2-8Si.js +0 -809
|
@@ -55,6 +55,11 @@ var import_resultCode = require("./resultCode");
|
|
|
55
55
|
var import_mouseActionTool = require("./mouseActionTool");
|
|
56
56
|
var import_visualSnapshotTool = require("./visualSnapshotTool");
|
|
57
57
|
var import_gojsTool = require("./gojsTool");
|
|
58
|
+
var import_dataSafetyGuard = require("./dataSafetyGuard");
|
|
59
|
+
var import_dataSafetySession = require("./dataSafetySession");
|
|
60
|
+
var import_finishTestSetupTool = require("./finishTestSetupTool");
|
|
61
|
+
var import_recordingPhase = require("./recordingPhase");
|
|
62
|
+
var import_mouseActions = require("./common/mouseActions");
|
|
58
63
|
var import_cssValue = require("./common/cssValue");
|
|
59
64
|
var import_visualSnapshot = require("./common/visualSnapshot");
|
|
60
65
|
var import_snapshotLocators = require("./snapshotLocators");
|
|
@@ -63,6 +68,7 @@ var import_tableSelectorBuilder = require("./tableSelectorBuilder");
|
|
|
63
68
|
var import_types = require("./types");
|
|
64
69
|
var import_dynamicId = require("../../dom-analyzer/dynamicId");
|
|
65
70
|
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
|
|
71
|
+
const PASSIVE_TOOLS = /* @__PURE__ */ new Set(["browser_snapshot", "browser_wait_for", "browser_take_screenshot", "browser_console_messages", "browser_network_requests"]);
|
|
66
72
|
function selectorIdentity(s) {
|
|
67
73
|
if (!s)
|
|
68
74
|
return null;
|
|
@@ -89,11 +95,11 @@ const REF_RESOLVE_TIMEOUT_MS = 1e3;
|
|
|
89
95
|
const MAX_ANCESTOR_HOVERS = 8;
|
|
90
96
|
const ANCESTOR_WALK_BUDGET_MS = 1e4;
|
|
91
97
|
class TraceRecordingBackend {
|
|
92
|
-
// per-type baseline filename counter
|
|
93
98
|
constructor(options) {
|
|
94
99
|
// The attribute getByTestId() resolves against (configured or the default).
|
|
95
100
|
this._testIdAttribute = "data-testid";
|
|
96
101
|
this._trackedActions = [];
|
|
102
|
+
this._recordingPhase = new import_recordingPhase.RecordingPhaseState();
|
|
97
103
|
this._droppedActions = [];
|
|
98
104
|
this._initialized = false;
|
|
99
105
|
this._pageCount = 0;
|
|
@@ -105,10 +111,48 @@ class TraceRecordingBackend {
|
|
|
105
111
|
this._reloading = false;
|
|
106
112
|
// true while page.reload() is in progress — suppresses spurious popup tracking
|
|
107
113
|
this._visualSnapshotCounters = { page: 0, element: 0, region: 0, gojsDiagram: 0 };
|
|
114
|
+
this._dataSafetyReady = false;
|
|
115
|
+
// Action ids of passive tool calls (see PASSIVE_TOOLS) in this recording:
|
|
116
|
+
// a create that started during one came from the app on its own.
|
|
117
|
+
this._passiveActionIds = /* @__PURE__ */ new Set();
|
|
108
118
|
this._options = options || {};
|
|
109
119
|
this._outputDir = options?.outputDir || process.cwd();
|
|
110
120
|
this._tempDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "skyramp-trace-"));
|
|
111
121
|
this._harPath = import_path.default.join(this._tempDir, "skyramp_network.har");
|
|
122
|
+
const dataSafety = (0, import_dataSafetyGuard.parseDataSafetyConfig)(this._options.dataSafety, process.env.SKYRAMP_DATA_SAFETY);
|
|
123
|
+
const ledgerDir = dataSafety.ledgerDir;
|
|
124
|
+
let ledgerSeq = 0;
|
|
125
|
+
const nextLedgerPath = ledgerDir ? () => import_path.default.join(ledgerDir, `dataSafetyLedger-${process.pid}-${Date.now()}-${ledgerSeq++}.json`) : void 0;
|
|
126
|
+
this._guard = new import_dataSafetyGuard.DataSafetyGuard(
|
|
127
|
+
dataSafety,
|
|
128
|
+
nextLedgerPath ? nextLedgerPath() : import_path.default.join(this._tempDir, "dataSafetyLedger.json"),
|
|
129
|
+
() => this._recordingPhase.setupCompleted,
|
|
130
|
+
nextLedgerPath
|
|
131
|
+
);
|
|
132
|
+
this._session = new import_dataSafetySession.DataSafetySession(this._guard, () => this._recordingPhase.requestProvenance());
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* An unusable data-safety config part, or a ledger that cannot be
|
|
136
|
+
* written, fails every tool call: recording without it would be looser
|
|
137
|
+
* than the caller asked for, and a ledgerDir caller has no other record.
|
|
138
|
+
* Checked on the first call (no I/O in the constructor) and again until
|
|
139
|
+
* it passes, and again after any ledger write fails mid-session: the
|
|
140
|
+
* check rewrites the full in-memory ledger, so recovery loses nothing.
|
|
141
|
+
*/
|
|
142
|
+
_checkDataSafetyStartup() {
|
|
143
|
+
if (this._dataSafetyReady && !this._guard.persistFailed)
|
|
144
|
+
return void 0;
|
|
145
|
+
if (!this._session.active && !this._guard.config.startupError) {
|
|
146
|
+
this._dataSafetyReady = true;
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
this._guard.ensureLedgerWritable();
|
|
151
|
+
this._dataSafetyReady = true;
|
|
152
|
+
return void 0;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return e instanceof Error ? e.message : String(e);
|
|
155
|
+
}
|
|
112
156
|
}
|
|
113
157
|
get trackedActions() {
|
|
114
158
|
return this._trackedActions;
|
|
@@ -175,6 +219,8 @@ class TraceRecordingBackend {
|
|
|
175
219
|
channel: config.browser.launchOptions?.channel
|
|
176
220
|
});
|
|
177
221
|
this._browserBackend.context.onBrowserContextCreated = async (browserContext) => {
|
|
222
|
+
(0, import_dataSafetySession.assertNoNetworkPolicyCollision)(config.network, this._session.active);
|
|
223
|
+
await this._session.install(browserContext);
|
|
178
224
|
this._installPopupListener(browserContext);
|
|
179
225
|
const ctx = this._browserBackend.context;
|
|
180
226
|
if (ctx && !ctx.sessionLog) {
|
|
@@ -198,40 +244,156 @@ class TraceRecordingBackend {
|
|
|
198
244
|
}
|
|
199
245
|
async listTools() {
|
|
200
246
|
const browserTools = await this._browserBackend.listTools();
|
|
201
|
-
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertHiddenTool.assertHiddenMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)(), (0, import_mouseActionTool.mouseActionMcpTool)(), (0, import_tableAssertTool.tableAssertMcpTool)(), (0, import_visualSnapshotTool.visualSnapshotMcpTool)(), (0, import_gojsTool.gojsAddNodeMcpTool)(), (0, import_gojsTool.gojsLinkMcpTool)()];
|
|
247
|
+
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_finishTestSetupTool.finishTestSetupMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertHiddenTool.assertHiddenMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)(), (0, import_mouseActionTool.mouseActionMcpTool)(), (0, import_tableAssertTool.tableAssertMcpTool)(), (0, import_visualSnapshotTool.visualSnapshotMcpTool)(), (0, import_gojsTool.gojsAddNodeMcpTool)(), (0, import_gojsTool.gojsLinkMcpTool)()];
|
|
202
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Thin wrapper around _callToolInner so every tool call — not just
|
|
251
|
+
* the browser-tool common path — gets any pending data-safety block notice
|
|
252
|
+
* appended to ITS OWN result. Several browser-interaction tools return early
|
|
253
|
+
* from deep inside _callToolInner (browser_select_option and its combobox
|
|
254
|
+
* fallback, the browser_fill_form→select retry, the same-URL navigate→reload
|
|
255
|
+
* branch) and can each trigger network requests the guard blocks; draining
|
|
256
|
+
* notices here — once, after every call resolves — ensures a block is always
|
|
257
|
+
* attributed to the call that caused it, never delayed onto a later,
|
|
258
|
+
* unrelated tool result or dropped if no later call ever runs. Applies even
|
|
259
|
+
* when isError is true: a blocked navigation routinely surfaces as an error
|
|
260
|
+
* result, and that's exactly the case the notice needs to explain.
|
|
261
|
+
*
|
|
262
|
+
* _callToolInner can also THROW (e.g. exportZipSchema.inputSchema.parse
|
|
263
|
+
* on bad args) instead of returning a CallToolResult — without a catch here
|
|
264
|
+
* that path skipped consumeBlockNotices() entirely, leaving the pending
|
|
265
|
+
* notice to be silently re-attributed to whatever call happened to succeed
|
|
266
|
+
* next (or dropped if none ever did). The catch drains notices for the
|
|
267
|
+
* throwing call too and folds them into a synthesized error result;
|
|
268
|
+
* re-throws untouched when there's nothing to attribute.
|
|
269
|
+
*/
|
|
203
270
|
async callTool(name, args, progress) {
|
|
271
|
+
try {
|
|
272
|
+
const result = await this._callToolInner(name, args, progress);
|
|
273
|
+
const notice = this._guard.consumeBlockNotices();
|
|
274
|
+
if (notice)
|
|
275
|
+
result.content = [...result.content ?? [], { type: "text", text: notice }];
|
|
276
|
+
return result;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
const notice = this._guard.consumeBlockNotices();
|
|
279
|
+
if (notice)
|
|
280
|
+
return { content: [{ type: "text", text: `### Error
|
|
281
|
+
${String(e)}${notice}` }], isError: true };
|
|
282
|
+
throw e;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async _callToolInner(name, args, progress) {
|
|
204
286
|
if (!this._initialized)
|
|
205
287
|
return { content: [{ type: "text", text: "### Error\nBackend not initialized. Call initialize() first." }], isError: true };
|
|
288
|
+
const dataSafetyError = this._checkDataSafetyStartup();
|
|
289
|
+
if (dataSafetyError)
|
|
290
|
+
return { content: [{ type: "text", text: `### Error
|
|
291
|
+
${dataSafetyError}
|
|
292
|
+
No browser action was run.` }], isError: true };
|
|
293
|
+
if (name === import_finishTestSetupTool.finishTestSetupSchema.name) {
|
|
294
|
+
import_finishTestSetupTool.finishTestSetupSchema.inputSchema.parse(args || {});
|
|
295
|
+
if (!this._recordingPhase.setupCompleted && this._guard.teardownStarted) {
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: "### Error\nskyramp_finish_test_setup must be called before the scenario steps. Data safety teardown already ran for this recording and deleted the records it created, so the boundary cannot be set now. Finish skyramp_export_zip (it refuses the recording and resets it), then record again from the start: setup steps, skyramp_finish_test_setup, then the scenario." }],
|
|
298
|
+
isError: true
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const changed = this._recordingPhase.finishSetup();
|
|
302
|
+
if (changed && this._session.active)
|
|
303
|
+
this._guard.flush();
|
|
304
|
+
return {
|
|
305
|
+
content: [{
|
|
306
|
+
type: "text",
|
|
307
|
+
text: changed ? "### Result\nScenario recording started." : "### Result\nScenario recording had already started."
|
|
308
|
+
}]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
206
311
|
if (name === import_exportTool.exportZipSchema.name) {
|
|
207
312
|
const parsed = import_exportTool.exportZipSchema.inputSchema.parse(args || {});
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
313
|
+
const prep = await this._session.prepareExport();
|
|
314
|
+
if (prep.kind === "capture-pending") {
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: `### Error
|
|
317
|
+
Data safety could not settle ${prep.urls.length} in-flight create response(s) before export \u2014 the following request(s) were still being captured when the settle timeout elapsed:
|
|
318
|
+
${prep.manifest}
|
|
319
|
+
The recording was NOT exported. Retry skyramp_export_zip once the response(s) complete, or check dataSafetyLedger.json for the audit trail.` }],
|
|
320
|
+
isError: true
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (prep.kind === "context-closed") {
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: "text", text: `### Error
|
|
326
|
+
Data safety teardown could not run: the browser context is already closed, so ${prep.pending.length} created entities could not be verified as deleted:
|
|
327
|
+
${prep.manifest}
|
|
328
|
+
Re-open the target with browser_navigate (to restore an authenticated session) and retry skyramp_export_zip, or delete these records manually.` }],
|
|
329
|
+
isError: true
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (prep.kind === "leftovers") {
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: `### Error
|
|
335
|
+
Data safety teardown could not delete every entity created during recording. The following records were left behind in the target environment:
|
|
336
|
+
${prep.manifest}
|
|
337
|
+
The recording was NOT exported. Delete these records manually or re-record against an ephemeral target.` }],
|
|
338
|
+
isError: true
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const unverified = this._guard.unparsedCreates.filter((u) => u.rule === "capture-timeout" || u.rule === "capture-failed");
|
|
342
|
+
if (unverified.length && !parsed.acknowledgeUnverifiedCreates) {
|
|
343
|
+
const manifest = (0, import_dataSafetyGuard.renderCappedList)(unverified.map((u) => `- POST ${u.url} [${u.rule}]`), "unverified create(s) \u2014 see dataSafetyLedger.json.");
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: "text", text: `### Error
|
|
346
|
+
Data safety: ${unverified.length} create request(s) may have reached the server, but their responses could not be recorded, so teardown could not delete what they created:
|
|
347
|
+
${manifest}
|
|
348
|
+
The recording was NOT exported. Tell the user these requests may have left records behind, then retry skyramp_export_zip with acknowledgeUnverifiedCreates: true.` }],
|
|
349
|
+
isError: true
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
this._resolveSourceActions();
|
|
353
|
+
const provenance2 = (0, import_exportTool.validateResourceProvenance)(this._trackedActions, this._guard.ledger, this._recordingPhase.setupCompleted);
|
|
354
|
+
if (!provenance2.ok)
|
|
355
|
+
return this._refuseAndResetRecording(provenance2.message);
|
|
356
|
+
await this._closeBrowserForExport();
|
|
357
|
+
const lateSettle = await this._session.settleBodyReads();
|
|
358
|
+
if (lateSettle.timedOut) {
|
|
359
|
+
return {
|
|
360
|
+
content: [{ type: "text", text: `### Error
|
|
361
|
+
Data safety: ${lateSettle.unsettled} create response(s) were still being captured after the browser closed.
|
|
362
|
+
The recording was NOT exported. Retry skyramp_export_zip once the response(s) complete, or check dataSafetyLedger.json for the audit trail.` }],
|
|
363
|
+
isError: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const lateCreates = this._guard.ledger.filter((e) => !e.tornDown);
|
|
367
|
+
if (lateCreates.length) {
|
|
368
|
+
const manifest = (0, import_dataSafetyGuard.renderCappedList)(lateCreates.map((e) => `- ${e.entityUrl} (id ${e.id})`), "left-behind entities \u2014 see dataSafetyLedger.json.");
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: `### Error
|
|
371
|
+
Data safety: ${lateCreates.length} record(s) were created while the browser was closing, so teardown could not delete them:
|
|
372
|
+
${manifest}
|
|
373
|
+
The recording was NOT exported. Delete these records manually.` }],
|
|
374
|
+
isError: true
|
|
375
|
+
};
|
|
376
|
+
}
|
|
219
377
|
this._consumeRecorderSelectors();
|
|
378
|
+
this._resolveSourceActions();
|
|
379
|
+
const serializedProvenance = (0, import_exportTool.validateResourceProvenance)(this._trackedActions, this._guard.ledger, this._recordingPhase.setupCompleted);
|
|
380
|
+
if (!serializedProvenance.ok)
|
|
381
|
+
return this._refuseAndResetRecording(serializedProvenance.message);
|
|
220
382
|
const handler = (0, import_exportTool.createExportZipHandler)({
|
|
221
383
|
trackedActions: this._trackedActions,
|
|
222
384
|
droppedActions: this._droppedActions,
|
|
223
385
|
harPath: this._harPath,
|
|
224
|
-
outputDir: this._outputDir
|
|
386
|
+
outputDir: this._outputDir,
|
|
387
|
+
// The ledger (and unparsedCreates advisory) rides the trace zip
|
|
388
|
+
// whenever the guard could have done something (strict, or
|
|
389
|
+
// offLimits blocks even in mode 'off'). By this point prepareExport()
|
|
390
|
+
// already returned 'clean', so this is never the leftovers/dead-context
|
|
391
|
+
// manifest — just the audit trail for a successful export.
|
|
392
|
+
dataSafety: this._session.active ? { ledgerJson: this._guard.ledgerJson(this._recordingPhase.setupCompleted), unparsedCreates: this._guard.unparsedCreates } : void 0
|
|
225
393
|
});
|
|
226
394
|
const exportResult = await handler(parsed);
|
|
227
|
-
if (!exportResult.isError)
|
|
228
|
-
this.
|
|
229
|
-
this._droppedActions = [];
|
|
230
|
-
const ra = this._browserBackend.context?.recorderActions;
|
|
231
|
-
if (ra)
|
|
232
|
-
ra.length = 0;
|
|
233
|
-
this._cleanupTempDir(true);
|
|
234
|
-
}
|
|
395
|
+
if (!exportResult.isError)
|
|
396
|
+
this._resetRecording();
|
|
235
397
|
return exportResult;
|
|
236
398
|
}
|
|
237
399
|
if (name === import_loadTraceTool.loadTraceSchema.name) {
|
|
@@ -244,47 +406,56 @@ class TraceRecordingBackend {
|
|
|
244
406
|
}
|
|
245
407
|
if (name === import_visualSnapshotTool.visualSnapshotSchema.name) {
|
|
246
408
|
const parsed = import_visualSnapshotTool.visualSnapshotSchema.inputSchema.parse(args || {});
|
|
247
|
-
return this._handleVisualSnapshot(parsed);
|
|
409
|
+
return this._withActionProvenance((provenance2) => this._handleVisualSnapshot(parsed, provenance2));
|
|
248
410
|
}
|
|
249
411
|
if (name === import_assertTool.assertToolSchema.name) {
|
|
250
412
|
const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
|
|
251
|
-
if (parsed.type === "order")
|
|
252
|
-
|
|
253
|
-
|
|
413
|
+
if (parsed.type === "order") {
|
|
414
|
+
const orderParams = parsed;
|
|
415
|
+
return this._withActionProvenance((provenance2) => this._handleAssertOrder(orderParams, provenance2));
|
|
416
|
+
}
|
|
417
|
+
const assertParams = { ...parsed, type: parsed.type };
|
|
418
|
+
return this._withActionProvenance((provenance2) => this._handleAssert(assertParams, provenance2));
|
|
254
419
|
}
|
|
255
420
|
if (name === import_assertHiddenTool.assertHiddenToolSchema.name) {
|
|
256
421
|
const parsed = import_assertHiddenTool.assertHiddenToolSchema.inputSchema.parse(args || {});
|
|
257
|
-
return this._handleAssertHidden(parsed, Date.now());
|
|
422
|
+
return this._withActionProvenance((provenance2) => this._handleAssertHidden(parsed, Date.now(), provenance2));
|
|
258
423
|
}
|
|
259
424
|
if (name === import_assertApiRequestTool.assertApiRequestSchema.name) {
|
|
260
|
-
this.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
425
|
+
return this._withActionProvenance(async (provenance2) => {
|
|
426
|
+
this._recordAction({
|
|
427
|
+
toolName: "browser_assert_api_request",
|
|
428
|
+
args: {},
|
|
429
|
+
code: "",
|
|
430
|
+
timestamp: Date.now(),
|
|
431
|
+
pageAlias: this._currentPageAlias
|
|
432
|
+
}, provenance2);
|
|
433
|
+
traceDebug("Assert API request marker recorded");
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: "### Result\nAPI request assertion marker recorded. The next action that triggers a network request will have its payload captured for validation in the generated test." }]
|
|
436
|
+
};
|
|
266
437
|
});
|
|
267
|
-
traceDebug("Assert API request marker recorded");
|
|
268
|
-
return {
|
|
269
|
-
content: [{ type: "text", text: "### Result\nAPI request assertion marker recorded. The next action that triggers a network request will have its payload captured for validation in the generated test." }]
|
|
270
|
-
};
|
|
271
438
|
}
|
|
272
439
|
if (name === import_tableAssertTool.tableAssertToolSchema.name) {
|
|
273
440
|
const parsed = import_tableAssertTool.tableAssertToolSchema.inputSchema.parse(args || {});
|
|
274
|
-
return this._handleAssertTableCell(parsed);
|
|
441
|
+
return this._withActionProvenance((provenance2) => this._handleAssertTableCell(parsed, provenance2));
|
|
275
442
|
}
|
|
276
443
|
if (name === import_gojsTool.gojsAddNodeSchema.name) {
|
|
277
444
|
const parsed = import_gojsTool.gojsAddNodeSchema.inputSchema.parse(args || {});
|
|
278
|
-
return this._handleGoJSAddNode(parsed);
|
|
445
|
+
return this._withActionProvenance((provenance2) => this._handleGoJSAddNode(parsed, provenance2));
|
|
279
446
|
}
|
|
280
447
|
if (name === import_gojsTool.gojsLinkSchema.name) {
|
|
281
448
|
const parsed = import_gojsTool.gojsLinkSchema.inputSchema.parse(args || {});
|
|
282
|
-
return this._handleGoJSLink(parsed);
|
|
449
|
+
return this._withActionProvenance((provenance2) => this._handleGoJSLink(parsed, provenance2));
|
|
283
450
|
}
|
|
284
451
|
if (name === "browser_select_option") {
|
|
285
452
|
const result2 = await this._handleSelectOption(args || {});
|
|
286
453
|
return result2;
|
|
287
454
|
}
|
|
455
|
+
if (name === "browser_fill_form")
|
|
456
|
+
return this._handleFillForm(args || {});
|
|
457
|
+
if ((name === "browser_type" || name === "browser_press_sequentially") && args?.submit)
|
|
458
|
+
return this._handleTypeWithSubmit(name, args || {});
|
|
288
459
|
if (name === "browser_tabs" && ["select", "switch"].includes(args?.action)) {
|
|
289
460
|
const index = args?.index ?? 0;
|
|
290
461
|
const targetUrl = this._browserBackend.context?.tabs()[index]?.page.url();
|
|
@@ -302,21 +473,26 @@ class TraceRecordingBackend {
|
|
|
302
473
|
const ts = Date.now();
|
|
303
474
|
const reloadAlias = this._currentPageAlias;
|
|
304
475
|
const tab = this._browserBackend.context.currentTab();
|
|
305
|
-
|
|
476
|
+
const provenance2 = this._recordingPhase.beginAction();
|
|
306
477
|
try {
|
|
307
|
-
|
|
478
|
+
this._reloading = true;
|
|
479
|
+
try {
|
|
480
|
+
await tab.page.reload({ waitUntil: "domcontentloaded" });
|
|
481
|
+
} finally {
|
|
482
|
+
this._reloading = false;
|
|
483
|
+
this._currentPageAlias = reloadAlias;
|
|
484
|
+
this._pendingPopupAlias = null;
|
|
485
|
+
}
|
|
486
|
+
this._recordAction({
|
|
487
|
+
toolName: "browser_navigate",
|
|
488
|
+
args: { url: currentUrl },
|
|
489
|
+
code: "",
|
|
490
|
+
timestamp: ts,
|
|
491
|
+
pageAlias: this._currentPageAlias
|
|
492
|
+
}, provenance2);
|
|
308
493
|
} finally {
|
|
309
|
-
this.
|
|
310
|
-
this._currentPageAlias = reloadAlias;
|
|
311
|
-
this._pendingPopupAlias = null;
|
|
494
|
+
this._recordingPhase.endAction(provenance2.actionId);
|
|
312
495
|
}
|
|
313
|
-
this._trackedActions.push({
|
|
314
|
-
toolName: "browser_navigate",
|
|
315
|
-
args: { url: currentUrl },
|
|
316
|
-
code: "",
|
|
317
|
-
timestamp: ts,
|
|
318
|
-
pageAlias: this._currentPageAlias
|
|
319
|
-
});
|
|
320
496
|
await this._enableFlutterAccessibility();
|
|
321
497
|
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
322
498
|
return {
|
|
@@ -332,6 +508,7 @@ Reloaded current page: ${currentUrl}
|
|
|
332
508
|
const timestampBeforeAction = Date.now();
|
|
333
509
|
const pageAliasBeforeAction = this._currentPageAlias;
|
|
334
510
|
const stripRestore = await this._maybeStripWrapperRoles(name, args);
|
|
511
|
+
const provenance = this._recordingPhase.beginAction();
|
|
335
512
|
let preClickHoverCode = null;
|
|
336
513
|
let preClickSnapshot = null;
|
|
337
514
|
let preClickLocatorCount = 0;
|
|
@@ -407,37 +584,38 @@ Reloaded current page: ${currentUrl}
|
|
|
407
584
|
}
|
|
408
585
|
result = await this._browserBackend.callTool(name, args);
|
|
409
586
|
} finally {
|
|
410
|
-
|
|
411
|
-
|
|
587
|
+
try {
|
|
588
|
+
if (stripRestore)
|
|
589
|
+
await stripRestore();
|
|
590
|
+
} finally {
|
|
591
|
+
this._recordingPhase.endAction(provenance.actionId);
|
|
592
|
+
}
|
|
412
593
|
}
|
|
413
594
|
if (name === "browser_navigate" && !result.isError) {
|
|
414
595
|
await this._enableFlutterAccessibility();
|
|
415
596
|
}
|
|
416
597
|
result = await this._improveActionSelector(name, args || {}, result, preClickHoverCode, preClickSnapshot, preClickLocatorCount, preClickParentChain);
|
|
417
|
-
if (name
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const fields = args?.fields;
|
|
421
|
-
if (fields) {
|
|
422
|
-
const comboField = fields.find((f) => f.type === "combobox");
|
|
423
|
-
if (comboField) {
|
|
424
|
-
traceDebug("fill_form failed on combobox field, retrying with select fallback");
|
|
425
|
-
this._maybeTrackAction(name, args || {}, { content: result.content, isError: false }, timestampBeforeAction, pageAliasBeforeAction);
|
|
426
|
-
const selectResult = await this._handleSelectOption({
|
|
427
|
-
element: comboField.name || "dropdown",
|
|
428
|
-
ref: comboField.ref,
|
|
429
|
-
values: [comboField.value]
|
|
430
|
-
});
|
|
431
|
-
return selectResult;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
this._maybeTrackAction(name, args || {}, result, timestampBeforeAction, pageAliasBeforeAction);
|
|
598
|
+
if (PASSIVE_TOOLS.has(name) && !result.isError)
|
|
599
|
+
this._passiveActionIds.add(provenance.actionId);
|
|
600
|
+
this._maybeTrackAction(name, args || {}, result, provenance, timestampBeforeAction, pageAliasBeforeAction);
|
|
437
601
|
return result;
|
|
438
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* The MCP SDK's server 'close' listener calls this synchronously
|
|
605
|
+
* (`ServerBackend.serverClosed?(server): void` — sdk/server.ts's
|
|
606
|
+
* `addServerListener(server, 'close', () => backend.serverClosed?.(server))`
|
|
607
|
+
* never awaits it), so the underlying work is unavoidably fire-and-forget
|
|
608
|
+
* from THAT caller's perspective. What was missing is any way for
|
|
609
|
+
* anything else — tests, or a future dispose/close path this class grows
|
|
610
|
+
* — to observe when the work actually finishes: the promise is now
|
|
611
|
+
* captured on the instance instead of discarded.
|
|
612
|
+
*/
|
|
439
613
|
serverClosed() {
|
|
440
|
-
|
|
614
|
+
this._autoExportPromise = this._autoExportAndClose().catch(import_log.logUnhandledError);
|
|
615
|
+
}
|
|
616
|
+
/** The in-flight (or settled) promise from the most recent serverClosed() call, if any — see the field comment. */
|
|
617
|
+
get autoExportPromise() {
|
|
618
|
+
return this._autoExportPromise;
|
|
441
619
|
}
|
|
442
620
|
/**
|
|
443
621
|
* Execute a coordinate-based mouse action and track each decomposed sub-action
|
|
@@ -448,23 +626,85 @@ Reloaded current page: ${currentUrl}
|
|
|
448
626
|
*/
|
|
449
627
|
async _handleMouseAction(params) {
|
|
450
628
|
const page = this._browserBackend.context?.currentTab()?.page;
|
|
629
|
+
const built = (0, import_mouseActions.mouseActionToJsonl)(params);
|
|
630
|
+
if ("error" in built)
|
|
631
|
+
return { content: [{ type: "text", text: `### Error
|
|
632
|
+
${built.error}` }], isError: true };
|
|
633
|
+
if (!page)
|
|
634
|
+
return { content: [{ type: "text", text: "### Error\nNo open page. Call browser_navigate first." }], isError: true };
|
|
451
635
|
const timestamp = Date.now();
|
|
452
636
|
const pageAlias = this._currentPageAlias;
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
637
|
+
const recorded = [];
|
|
638
|
+
const code = [];
|
|
639
|
+
try {
|
|
640
|
+
for (const action of built.actions) {
|
|
641
|
+
const provenance = this._recordingPhase.beginAction();
|
|
642
|
+
try {
|
|
643
|
+
code.push(...(0, import_mouseActions.mouseJsonlToCode)(action));
|
|
644
|
+
await this._runMouseSubAction(page, action);
|
|
645
|
+
recorded.push({ action, provenance });
|
|
646
|
+
} finally {
|
|
647
|
+
this._recordingPhase.endAction(provenance.actionId);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
return { content: [{ type: "text", text: `### Error
|
|
652
|
+
Mouse action failed: ${e.message}` }], isError: true };
|
|
653
|
+
}
|
|
654
|
+
recorded.forEach(({ action, provenance }, i) => {
|
|
655
|
+
this._recordAction({
|
|
458
656
|
toolName: "browser_mouse_action",
|
|
459
|
-
args:
|
|
657
|
+
args: action,
|
|
460
658
|
code: "",
|
|
461
659
|
// Stagger timestamps so a decomposed drag keeps its move<down<move<up order.
|
|
462
660
|
timestamp: timestamp + i,
|
|
463
661
|
pageAlias
|
|
464
|
-
});
|
|
662
|
+
}, provenance);
|
|
465
663
|
});
|
|
466
|
-
traceDebug(`Tracked ${
|
|
467
|
-
return
|
|
664
|
+
traceDebug(`Tracked ${recorded.length} mouse sub-action(s) for "${params.action}" on ${pageAlias}`);
|
|
665
|
+
return { content: [{ type: "text", text: `### Ran Playwright code
|
|
666
|
+
${code.join("\n")}` }] };
|
|
667
|
+
}
|
|
668
|
+
async _runMouseSubAction(page, action) {
|
|
669
|
+
const withModifiers = async (mask, operation) => {
|
|
670
|
+
const keys = (0, import_mouseActions.decodeModifierKeys)(mask);
|
|
671
|
+
for (const key of keys)
|
|
672
|
+
await page.keyboard.down(key);
|
|
673
|
+
try {
|
|
674
|
+
await operation();
|
|
675
|
+
} finally {
|
|
676
|
+
for (const key of [...keys].reverse())
|
|
677
|
+
await page.keyboard.up(key);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
switch (action.name) {
|
|
681
|
+
case "mouse.move":
|
|
682
|
+
if (action.steps === void 0)
|
|
683
|
+
await page.mouse.move(action.position.x, action.position.y);
|
|
684
|
+
else
|
|
685
|
+
await page.mouse.move(action.position.x, action.position.y, { steps: action.steps });
|
|
686
|
+
return;
|
|
687
|
+
case "mouse.down":
|
|
688
|
+
if (action.button && action.button !== "left")
|
|
689
|
+
await page.mouse.down({ button: action.button });
|
|
690
|
+
else
|
|
691
|
+
await page.mouse.down();
|
|
692
|
+
return;
|
|
693
|
+
case "mouse.up":
|
|
694
|
+
if (action.button && action.button !== "left")
|
|
695
|
+
await page.mouse.up({ button: action.button });
|
|
696
|
+
else
|
|
697
|
+
await page.mouse.up();
|
|
698
|
+
return;
|
|
699
|
+
case "mouse.wheel":
|
|
700
|
+
await withModifiers(action.modifiers, () => page.mouse.wheel(action.deltaX, action.deltaY));
|
|
701
|
+
return;
|
|
702
|
+
case "click": {
|
|
703
|
+
const options = action.button !== "left" || action.clickCount !== 1 ? { button: action.button, clickCount: action.clickCount } : void 0;
|
|
704
|
+
await withModifiers(action.modifiers, () => options ? page.mouse.click(action.position.x, action.position.y, options) : page.mouse.click(action.position.x, action.position.y));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
468
708
|
}
|
|
469
709
|
/**
|
|
470
710
|
* Handle browser_visual_snapshot: record a `visualSnapshot` marker that
|
|
@@ -486,7 +726,7 @@ Reloaded current page: ${currentUrl}
|
|
|
486
726
|
* (same machinery as the GoJS tools). Iframe-element snapshots from the recorder
|
|
487
727
|
* remain out of scope here (see common/visualSnapshot.ts).
|
|
488
728
|
*/
|
|
489
|
-
async _handleVisualSnapshot(params) {
|
|
729
|
+
async _handleVisualSnapshot(params, provenance) {
|
|
490
730
|
const timestamp = Date.now();
|
|
491
731
|
const pageAlias = this._currentPageAlias;
|
|
492
732
|
const input = {
|
|
@@ -522,13 +762,13 @@ GoJS diagram "${diagramSelector}" is not inside an iframe; the gojsDiagram snaps
|
|
|
522
762
|
${built.error}` }], isError: true };
|
|
523
763
|
this._visualSnapshotCounters[params.snapshotType]++;
|
|
524
764
|
this._advanceSnapshotCounterFor(params.snapshotType, input.filename);
|
|
525
|
-
this.
|
|
765
|
+
this._recordAction({
|
|
526
766
|
toolName: "browser_visual_snapshot",
|
|
527
767
|
args: built.action,
|
|
528
768
|
code: "",
|
|
529
769
|
timestamp,
|
|
530
770
|
pageAlias
|
|
531
|
-
});
|
|
771
|
+
}, provenance);
|
|
532
772
|
traceDebug(`Tracked visualSnapshot (${params.snapshotType}) "${input.filename}" on ${pageAlias}`);
|
|
533
773
|
const targetDesc = params.snapshotType === "element" ? ` (${input.selector})` : params.snapshotType === "region" && params.clip ? ` (clip ${params.clip.width}x${params.clip.height} at ${params.clip.x},${params.clip.y})` : params.snapshotType === "gojsDiagram" ? ` (${input.diagramSelector} via ${input.iframeSelectors?.length ?? 0} iframe(s))` : params.fullPage ? " (full page)" : "";
|
|
534
774
|
return { content: [{ type: "text", text: `### Visual snapshot recorded
|
|
@@ -686,11 +926,17 @@ The selected range acts on tab "${a.frame.pageAlias}" but the step that opens th
|
|
|
686
926
|
if (seedPage)
|
|
687
927
|
claimedTabs.add(seedPage);
|
|
688
928
|
let tabsBefore = [];
|
|
929
|
+
const replayProvenance = /* @__PURE__ */ new Map();
|
|
930
|
+
let activeReplayProvenance;
|
|
689
931
|
let result;
|
|
690
932
|
try {
|
|
691
933
|
result = await (0, import_loadTraceTool.replayActions)(loadedActions, {
|
|
692
934
|
pageForAlias: (alias) => remap.get(alias)?.page ?? this._pageForAlias(alias),
|
|
693
|
-
beforeAction: () => {
|
|
935
|
+
beforeAction: (action) => {
|
|
936
|
+
if (activeReplayProvenance)
|
|
937
|
+
this._recordingPhase.endAction(activeReplayProvenance.actionId);
|
|
938
|
+
activeReplayProvenance = this._recordingPhase.beginAction();
|
|
939
|
+
replayProvenance.set(action, activeReplayProvenance);
|
|
694
940
|
tabsBefore = liveTabs();
|
|
695
941
|
},
|
|
696
942
|
afterAction: async (action) => {
|
|
@@ -709,11 +955,18 @@ The selected range acts on tab "${a.frame.pageAlias}" but the step that opens th
|
|
|
709
955
|
claimedTabs.add(fresh);
|
|
710
956
|
remap.set(signal.popupAlias, { page: fresh, liveAlias: this._currentPageAlias });
|
|
711
957
|
}
|
|
958
|
+
if (activeReplayProvenance) {
|
|
959
|
+
this._recordingPhase.endAction(activeReplayProvenance.actionId);
|
|
960
|
+
activeReplayProvenance = void 0;
|
|
961
|
+
}
|
|
712
962
|
}
|
|
713
963
|
}, params);
|
|
714
964
|
} catch (e) {
|
|
715
965
|
return { content: [{ type: "text", text: `### Error
|
|
716
966
|
Replay failed: ${e.message}` }], isError: true };
|
|
967
|
+
} finally {
|
|
968
|
+
if (activeReplayProvenance)
|
|
969
|
+
this._recordingPhase.endAction(activeReplayProvenance.actionId);
|
|
717
970
|
}
|
|
718
971
|
for (let i = result.startIndex; i < result.stopIndex; i++) {
|
|
719
972
|
const a = loadedActions[i];
|
|
@@ -721,7 +974,10 @@ Replay failed: ${e.message}` }], isError: true };
|
|
|
721
974
|
if (liveAlias && liveAlias !== a.frame.pageAlias)
|
|
722
975
|
a.frame = { ...a.frame, pageAlias: liveAlias };
|
|
723
976
|
const before = this._trackedActions.length;
|
|
724
|
-
|
|
977
|
+
const provenance = replayProvenance.get(a);
|
|
978
|
+
if (!provenance)
|
|
979
|
+
continue;
|
|
980
|
+
this._seedTrackedAction(a, provenance);
|
|
725
981
|
const popupSignal = a.action.signals.find((s) => s.name === "popup");
|
|
726
982
|
if (popupSignal && this._trackedActions.length > before)
|
|
727
983
|
this._trackedActions[this._trackedActions.length - 1].popupAlias = remap.get(popupSignal.popupAlias)?.liveAlias ?? popupSignal.popupAlias;
|
|
@@ -774,17 +1030,17 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
774
1030
|
* re-derives the JSONL selector from the `code` field, so we regenerate clean
|
|
775
1031
|
* Playwright code from the stored internal: selector via asLocator.
|
|
776
1032
|
*/
|
|
777
|
-
_seedTrackedAction(action) {
|
|
1033
|
+
_seedTrackedAction(action, provenance) {
|
|
778
1034
|
const a = action.action;
|
|
779
1035
|
const pageAlias = action.frame.pageAlias || "page";
|
|
780
1036
|
if (a.name === "navigate") {
|
|
781
|
-
this.
|
|
1037
|
+
this._recordAction({
|
|
782
1038
|
toolName: "browser_navigate",
|
|
783
1039
|
args: { url: a.url },
|
|
784
1040
|
code: "",
|
|
785
1041
|
timestamp: action.startTime,
|
|
786
1042
|
pageAlias
|
|
787
|
-
});
|
|
1043
|
+
}, provenance);
|
|
788
1044
|
return;
|
|
789
1045
|
}
|
|
790
1046
|
const locatorExpr = a.selector ? (0, import_utils.asLocator)("javascript", a.selector) : "";
|
|
@@ -793,12 +1049,12 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
793
1049
|
return;
|
|
794
1050
|
if (seeded.toolName === "browser_visual_snapshot")
|
|
795
1051
|
this._advanceSnapshotCounterFor(seeded.args.snapshotType, seeded.args.filename);
|
|
796
|
-
this.
|
|
1052
|
+
this._recordAction({
|
|
797
1053
|
...seeded,
|
|
798
1054
|
timestamp: action.startTime,
|
|
799
1055
|
pageAlias,
|
|
800
1056
|
framePath: action.frame.framePath?.length ? action.frame.framePath : void 0
|
|
801
|
-
});
|
|
1057
|
+
}, provenance);
|
|
802
1058
|
}
|
|
803
1059
|
/**
|
|
804
1060
|
* Bump the per-type snapshot counter to at least the number embedded in a
|
|
@@ -1028,7 +1284,9 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
1028
1284
|
* take a snapshot to find the options, then click the matching option.
|
|
1029
1285
|
*/
|
|
1030
1286
|
async _handleSelectOption(args) {
|
|
1031
|
-
const result = await this.
|
|
1287
|
+
const { result, provenance } = await this._runActionOperation(
|
|
1288
|
+
() => this._browserBackend.callTool("browser_select_option", args)
|
|
1289
|
+
);
|
|
1032
1290
|
const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
1033
1291
|
if (!result.isError) {
|
|
1034
1292
|
const code = (0, import_response.parseResponse)(result)?.code ?? "";
|
|
@@ -1050,18 +1308,18 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
1050
1308
|
const selectCode = `await page.${locatorExpr}.selectOption(${JSON.stringify(values2.length === 1 ? values2[0] : values2)});`;
|
|
1051
1309
|
traceDebug(`Improved select selector: ${selectCode}`);
|
|
1052
1310
|
const timestamp = Date.now();
|
|
1053
|
-
this.
|
|
1311
|
+
this._recordAction({
|
|
1054
1312
|
toolName: "browser_select_option",
|
|
1055
1313
|
args,
|
|
1056
1314
|
code: selectCode,
|
|
1057
1315
|
timestamp
|
|
1058
|
-
});
|
|
1316
|
+
}, provenance);
|
|
1059
1317
|
return result;
|
|
1060
1318
|
}
|
|
1061
1319
|
}
|
|
1062
1320
|
}
|
|
1063
1321
|
}
|
|
1064
|
-
this._maybeTrackAction("browser_select_option", args, result);
|
|
1322
|
+
this._maybeTrackAction("browser_select_option", args, result, provenance);
|
|
1065
1323
|
return result;
|
|
1066
1324
|
}
|
|
1067
1325
|
const shouldFallback = resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout") || resultText.includes("not found in the current page snapshot");
|
|
@@ -1080,11 +1338,13 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
1080
1338
|
const comboboxes = [...snapText.matchAll(/combobox[^\n]*\[ref=(\w+)\]/g)];
|
|
1081
1339
|
const clickableCombo = comboboxes.find((m) => m[0].includes("cursor=pointer"));
|
|
1082
1340
|
const comboRef = clickableCombo?.[1] || args.ref;
|
|
1083
|
-
const
|
|
1341
|
+
const { result: clickResult, provenance: clickProvenance } = await this._runActionOperation(
|
|
1342
|
+
() => this._browserBackend.callTool("browser_click", { element: args.element || "dropdown", ref: comboRef })
|
|
1343
|
+
);
|
|
1084
1344
|
if (clickResult.isError)
|
|
1085
1345
|
return { content: [{ type: "text", text: `### Error
|
|
1086
1346
|
Failed to open custom dropdown. The dropdown modal may not be visible yet. Try calling browser_snapshot first to check the page state, then ensure the form/modal is open before selecting an option.` }], isError: true };
|
|
1087
|
-
this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult);
|
|
1347
|
+
this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult, clickProvenance);
|
|
1088
1348
|
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
1089
1349
|
if (snapResult.isError)
|
|
1090
1350
|
return snapResult;
|
|
@@ -1102,15 +1362,19 @@ Failed to open custom dropdown. The dropdown modal may not be visible yet. Try c
|
|
|
1102
1362
|
const pattern1 = new RegExp(`option\\s+"${escaped}"[^\\n]*\\[ref=(\\w+)\\]`, "i");
|
|
1103
1363
|
const match1 = snapText.match(pattern1);
|
|
1104
1364
|
if (match1) {
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1365
|
+
const { result: optionClickResult, provenance: optionProvenance } = await this._runActionOperation(
|
|
1366
|
+
() => this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match1[1] })
|
|
1367
|
+
);
|
|
1368
|
+
this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match1[1] }, optionClickResult, optionProvenance);
|
|
1107
1369
|
return optionClickResult;
|
|
1108
1370
|
}
|
|
1109
1371
|
const pattern2 = new RegExp(`option[^\\n]*\\[ref=(\\w+)\\][^\\n]*${escaped}`, "i");
|
|
1110
1372
|
const match2 = snapText.match(pattern2);
|
|
1111
1373
|
if (match2) {
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1374
|
+
const { result: optionClickResult, provenance: optionProvenance } = await this._runActionOperation(
|
|
1375
|
+
() => this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match2[1] })
|
|
1376
|
+
);
|
|
1377
|
+
this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match2[1] }, optionClickResult, optionProvenance);
|
|
1114
1378
|
return optionClickResult;
|
|
1115
1379
|
}
|
|
1116
1380
|
}
|
|
@@ -1119,6 +1383,82 @@ Failed to open custom dropdown. The dropdown modal may not be visible yet. Try c
|
|
|
1119
1383
|
Opened the custom dropdown but could not find a matching option for "${targetValue}". Available options:
|
|
1120
1384
|
${optionLines}` }], isError: true };
|
|
1121
1385
|
}
|
|
1386
|
+
async _handleFillForm(args) {
|
|
1387
|
+
const fields = args.fields;
|
|
1388
|
+
if (!fields?.length)
|
|
1389
|
+
return this._browserBackend.callTool("browser_fill_form", args);
|
|
1390
|
+
const content = [];
|
|
1391
|
+
for (const field of fields) {
|
|
1392
|
+
const fieldArgs = { fields: [field] };
|
|
1393
|
+
const timestamp = Date.now();
|
|
1394
|
+
const pageAlias = this._currentPageAlias;
|
|
1395
|
+
const { result, provenance } = await this._runActionOperation(
|
|
1396
|
+
() => this._browserBackend.callTool("browser_fill_form", fieldArgs)
|
|
1397
|
+
);
|
|
1398
|
+
if (result.isError) {
|
|
1399
|
+
const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
1400
|
+
if (field.type === "combobox" && (resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout"))) {
|
|
1401
|
+
const selectResult = await this._handleSelectOption({
|
|
1402
|
+
element: field.name || "dropdown",
|
|
1403
|
+
ref: field.ref,
|
|
1404
|
+
values: [field.value]
|
|
1405
|
+
});
|
|
1406
|
+
if (selectResult.isError)
|
|
1407
|
+
return { ...selectResult, content: [...content, ...selectResult.content ?? []] };
|
|
1408
|
+
content.push(...selectResult.content ?? []);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
return { ...result, content: [...content, ...result.content ?? []] };
|
|
1412
|
+
}
|
|
1413
|
+
this._maybeTrackAction("browser_fill_form", fieldArgs, result, provenance, timestamp, pageAlias);
|
|
1414
|
+
content.push(...result.content ?? []);
|
|
1415
|
+
}
|
|
1416
|
+
return { content };
|
|
1417
|
+
}
|
|
1418
|
+
async _handleTypeWithSubmit(name, args) {
|
|
1419
|
+
const tab = this._browserBackend.context?.currentTab();
|
|
1420
|
+
let submitTarget;
|
|
1421
|
+
const typeArgs = { ...args, submit: false };
|
|
1422
|
+
const typeTimestamp = Date.now();
|
|
1423
|
+
const typePageAlias = this._currentPageAlias;
|
|
1424
|
+
const { result: rawTypeResult, provenance: typeProvenance } = await this._runActionOperation(
|
|
1425
|
+
async () => {
|
|
1426
|
+
if (name === "browser_type" && tab) {
|
|
1427
|
+
try {
|
|
1428
|
+
submitTarget = await tab.refLocator({ element: args.element, ref: args.ref });
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
return { content: [{ type: "text", text: `### Error
|
|
1431
|
+
${String(error)}` }], isError: true };
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return this._browserBackend.callTool(name, typeArgs);
|
|
1435
|
+
}
|
|
1436
|
+
);
|
|
1437
|
+
const typeResult = await this._improveActionSelector(name, typeArgs, rawTypeResult, null, null, 0, null);
|
|
1438
|
+
this._maybeTrackAction(name, typeArgs, typeResult, typeProvenance, typeTimestamp, typePageAlias);
|
|
1439
|
+
if (typeResult.isError)
|
|
1440
|
+
return typeResult;
|
|
1441
|
+
const pressArgs = { key: "Enter" };
|
|
1442
|
+
const pressTimestamp = Date.now();
|
|
1443
|
+
const pressPageAlias = this._currentPageAlias;
|
|
1444
|
+
const { result: pressResult, provenance: pressProvenance } = await this._runActionOperation(
|
|
1445
|
+
async () => {
|
|
1446
|
+
if (!submitTarget || !tab)
|
|
1447
|
+
return this._browserBackend.callTool("browser_press_key", pressArgs);
|
|
1448
|
+
const response = import_response.Response.create(tab.context, "browser_press_key", pressArgs);
|
|
1449
|
+
response.setIncludeSnapshot();
|
|
1450
|
+
response.addCode(`await page.${submitTarget.resolved}.press('Enter');`);
|
|
1451
|
+
try {
|
|
1452
|
+
await tab.waitForCompletion(() => submitTarget.locator.press("Enter"));
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
response.addError(String(error));
|
|
1455
|
+
}
|
|
1456
|
+
return response.build();
|
|
1457
|
+
}
|
|
1458
|
+
);
|
|
1459
|
+
this._maybeTrackAction("browser_press_key", pressArgs, pressResult, pressProvenance, pressTimestamp, pressPageAlias);
|
|
1460
|
+
return { ...pressResult, content: [...typeResult.content ?? [], ...pressResult.content ?? []] };
|
|
1461
|
+
}
|
|
1122
1462
|
/**
|
|
1123
1463
|
* Find the Playwright Frame whose document hosts a GoJS diagram in the given
|
|
1124
1464
|
* container div (walks all frames; GoJS lives in nested same-origin iframes).
|
|
@@ -1197,7 +1537,7 @@ ${optionLines}` }], isError: true };
|
|
|
1197
1537
|
* record a diagramNodeAdd action carrying the semantic fields skyramp codegen
|
|
1198
1538
|
* consumes.
|
|
1199
1539
|
*/
|
|
1200
|
-
async _handleGoJSAddNode(params) {
|
|
1540
|
+
async _handleGoJSAddNode(params, provenance) {
|
|
1201
1541
|
const timestamp = Date.now();
|
|
1202
1542
|
const { frame, error: frameError } = await this._findGoJSFrame(params.diagramSelector);
|
|
1203
1543
|
if (!frame)
|
|
@@ -1275,13 +1615,13 @@ GoJS add node failed: ${result ? result.err : "unknown"}` }], isError: true };
|
|
|
1275
1615
|
anchorDocX: result.anchorDocX,
|
|
1276
1616
|
anchorDocY: result.anchorDocY
|
|
1277
1617
|
};
|
|
1278
|
-
this.
|
|
1618
|
+
this._recordAction({
|
|
1279
1619
|
toolName: import_gojsTool.gojsAddNodeSchema.name,
|
|
1280
1620
|
args,
|
|
1281
1621
|
code: "",
|
|
1282
1622
|
timestamp,
|
|
1283
1623
|
framePath
|
|
1284
|
-
});
|
|
1624
|
+
}, provenance);
|
|
1285
1625
|
traceDebug(`GoJS add node: ${params.category} key=${result.key} anchor=${params.anchorKey ?? "-"}`);
|
|
1286
1626
|
return { content: [{ type: "text", text: `### Result
|
|
1287
1627
|
Added GoJS node "${params.category}" (key ${result.key})${params.anchorKey ? ` near ${params.anchorKey}` : ""}.` }] };
|
|
@@ -1290,7 +1630,7 @@ Added GoJS node "${params.category}" (key ${result.key})${params.anchorKey ? ` n
|
|
|
1290
1630
|
* Handle browser_gojs_link: connect two GoJS nodes via the model API
|
|
1291
1631
|
* (mirroring the GoJSDiagram helper) and record a diagramLinkAdd action.
|
|
1292
1632
|
*/
|
|
1293
|
-
async _handleGoJSLink(params) {
|
|
1633
|
+
async _handleGoJSLink(params, provenance) {
|
|
1294
1634
|
const timestamp = Date.now();
|
|
1295
1635
|
const { frame, error: frameError } = await this._findGoJSFrame(params.diagramSelector);
|
|
1296
1636
|
if (!frame)
|
|
@@ -1336,13 +1676,13 @@ GoJS link failed: ${result ? result.err : "unknown"}` }], isError: true };
|
|
|
1336
1676
|
fromPort: params.fromPort,
|
|
1337
1677
|
toPort: params.toPort
|
|
1338
1678
|
};
|
|
1339
|
-
this.
|
|
1679
|
+
this._recordAction({
|
|
1340
1680
|
toolName: import_gojsTool.gojsLinkSchema.name,
|
|
1341
1681
|
args,
|
|
1342
1682
|
code: "",
|
|
1343
1683
|
timestamp,
|
|
1344
1684
|
framePath
|
|
1345
|
-
});
|
|
1685
|
+
}, provenance);
|
|
1346
1686
|
traceDebug(`GoJS link: ${params.fromKey} -> ${params.toKey} (${params.fromPort}->${params.toPort})`);
|
|
1347
1687
|
return { content: [{ type: "text", text: `### Result
|
|
1348
1688
|
Linked GoJS nodes ${params.fromKey} \u2192 ${params.toKey} (${params.fromPort}\u2192${params.toPort}).` }] };
|
|
@@ -1356,7 +1696,7 @@ Linked GoJS nodes ${params.fromKey} \u2192 ${params.toKey} (${params.fromPort}\u
|
|
|
1356
1696
|
* dispatched to _handleAssertOrder instead: two refs, a live boundingBox()
|
|
1357
1697
|
* comparison, no pointer-off read. Only passing assertions are recorded.
|
|
1358
1698
|
*/
|
|
1359
|
-
async _handleAssert(params) {
|
|
1699
|
+
async _handleAssert(params, provenance) {
|
|
1360
1700
|
const timestamp = Date.now();
|
|
1361
1701
|
if (!params.expected && (params.type === "text" || params.type === "value" || params.type === "count"))
|
|
1362
1702
|
return { content: [{ type: "text", text: '### Error\n"expected" parameter is required.' }], isError: true };
|
|
@@ -1620,8 +1960,8 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
|
|
|
1620
1960
|
break;
|
|
1621
1961
|
}
|
|
1622
1962
|
if (unhoverMove)
|
|
1623
|
-
this.
|
|
1624
|
-
this.
|
|
1963
|
+
this._recordAction({ toolName: "browser_mouse_action", args: { name: "mouse.move", position: unhoverMove }, code: "", timestamp: timestamp - 1, pageAlias: this._currentPageAlias }, provenance);
|
|
1964
|
+
this._recordAction({ toolName: "browser_assert", args, code, timestamp, pageAlias: this._currentPageAlias }, provenance);
|
|
1625
1965
|
traceDebug(`Assert: ${assertName} with selector ${args.selector}`);
|
|
1626
1966
|
}
|
|
1627
1967
|
return {
|
|
@@ -1640,7 +1980,7 @@ ${details}` }]
|
|
|
1640
1980
|
* above/below — compared strictly. It says where each element STARTS, not
|
|
1641
1981
|
* that one lies wholly beyond the other.
|
|
1642
1982
|
*/
|
|
1643
|
-
async _handleAssertOrder(params) {
|
|
1983
|
+
async _handleAssertOrder(params, provenance) {
|
|
1644
1984
|
const timestamp = Date.now();
|
|
1645
1985
|
const error = (text) => ({ content: [{ type: "text", text: `### Error
|
|
1646
1986
|
${text}` }], isError: true });
|
|
@@ -1700,7 +2040,7 @@ ${details}` }] };
|
|
|
1700
2040
|
otherLocator: b.parsed.locator,
|
|
1701
2041
|
relation
|
|
1702
2042
|
};
|
|
1703
|
-
this.
|
|
2043
|
+
this._recordAction({ toolName: "browser_assert", args, code: `assertOrder:${a.selector}:${relation}:${b.selector}`, timestamp, pageAlias: this._currentPageAlias }, provenance);
|
|
1704
2044
|
traceDebug(`Assert: assertOrder ${a.selector} ${relation} ${b.selector}`);
|
|
1705
2045
|
return { content: [{ type: "text", text: `### Assertion Passed
|
|
1706
2046
|
${details}${a.note}${b.note}` }] };
|
|
@@ -1711,7 +2051,7 @@ ${details}${a.note}${b.note}` }] };
|
|
|
1711
2051
|
* action. Column is 1-based over DATA columns (A=1); the leading row-number
|
|
1712
2052
|
* <th> is skipped via td:nth-child(column + 1). Row is the 1-based visible row.
|
|
1713
2053
|
*/
|
|
1714
|
-
async _handleAssertTableCell(params) {
|
|
2054
|
+
async _handleAssertTableCell(params, provenance) {
|
|
1715
2055
|
const timestamp = Date.now();
|
|
1716
2056
|
const tablePrefix = params.table ? `${params.table} ` : "";
|
|
1717
2057
|
const selector = `${tablePrefix}tbody tr:nth-child(${params.row}) td:nth-child(${params.column + 1})`;
|
|
@@ -1770,7 +2110,7 @@ No cell at row ${params.row}, column ${params.column} (selector: ${selector}).`
|
|
|
1770
2110
|
isInput: !!cell.isInput
|
|
1771
2111
|
});
|
|
1772
2112
|
if (passed) {
|
|
1773
|
-
this.
|
|
2113
|
+
this._recordAction({
|
|
1774
2114
|
toolName: import_tableAssertTool.tableAssertToolSchema.name,
|
|
1775
2115
|
// Record the active page so assertions on popup/secondary tabs are
|
|
1776
2116
|
// attributed correctly (mirrors _handleAssertApiRequest).
|
|
@@ -1785,7 +2125,7 @@ No cell at row ${params.row}, column ${params.column} (selector: ${selector}).`
|
|
|
1785
2125
|
},
|
|
1786
2126
|
code: "",
|
|
1787
2127
|
timestamp
|
|
1788
|
-
});
|
|
2128
|
+
}, provenance);
|
|
1789
2129
|
traceDebug(`AssertTableCell: ${selector} \u2192 "${params.expected}" (isInput=${cell.isInput})`);
|
|
1790
2130
|
}
|
|
1791
2131
|
return {
|
|
@@ -1870,7 +2210,7 @@ The current page (${url}) is not a loaded application page; navigate to the app
|
|
|
1870
2210
|
* assertActionToJsonl and the re-seed path share one shape with the other
|
|
1871
2211
|
* assertions.
|
|
1872
2212
|
*/
|
|
1873
|
-
async _handleAssertHidden(params, timestamp) {
|
|
2213
|
+
async _handleAssertHidden(params, timestamp, provenance) {
|
|
1874
2214
|
const err = (text) => ({ content: [{ type: "text", text: `### Error
|
|
1875
2215
|
${text}` }], isError: true });
|
|
1876
2216
|
const given = [params.testId, params.role, params.text, params.attribute].filter((v) => typeof v === "string" && v.trim() !== "");
|
|
@@ -1927,7 +2267,7 @@ ${text}` }], isError: true });
|
|
|
1927
2267
|
let details;
|
|
1928
2268
|
if (passed) {
|
|
1929
2269
|
details = `Hidden assertion passed: "${params.element}" is absent or not visible.`;
|
|
1930
|
-
this.
|
|
2270
|
+
this._recordAction({ toolName: "browser_assert", args: { type: "hidden", selector, locator }, code: `assertHidden:${selector}`, timestamp, pageAlias: this._currentPageAlias }, provenance);
|
|
1931
2271
|
traceDebug(`Assert: assertHidden with selector ${selector}`);
|
|
1932
2272
|
} else {
|
|
1933
2273
|
const count = await tab.page.locator(selector).count().catch(() => -1);
|
|
@@ -2683,7 +3023,87 @@ ${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFa
|
|
|
2683
3023
|
}
|
|
2684
3024
|
};
|
|
2685
3025
|
}
|
|
2686
|
-
|
|
3026
|
+
async _withActionProvenance(operation) {
|
|
3027
|
+
const provenance = this._recordingPhase.beginAction();
|
|
3028
|
+
try {
|
|
3029
|
+
return await operation(provenance);
|
|
3030
|
+
} finally {
|
|
3031
|
+
this._recordingPhase.endAction(provenance.actionId);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
async _runActionOperation(operation) {
|
|
3035
|
+
const provenance = this._recordingPhase.beginAction();
|
|
3036
|
+
try {
|
|
3037
|
+
return { result: await operation(), provenance };
|
|
3038
|
+
} finally {
|
|
3039
|
+
this._recordingPhase.endAction(provenance.actionId);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
_recordAction(action, provenance) {
|
|
3043
|
+
this._trackedActions.push({ ...action, ...provenance });
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Close the browser context to flush the HAR file to disk (Playwright
|
|
3047
|
+
* writes HAR on context close). Bounded: a heavy page (e.g. a
|
|
3048
|
+
* nested-iframe GoJS canvas with live timers) can stall context close
|
|
3049
|
+
* indefinitely. The HAR flush is best-effort; the JSONL trace (what
|
|
3050
|
+
* codegen consumes) is complete regardless.
|
|
3051
|
+
*/
|
|
3052
|
+
async _closeBrowserForExport() {
|
|
3053
|
+
const closePromise = this._browserBackend.callTool("browser_close", {}).catch(() => {
|
|
3054
|
+
});
|
|
3055
|
+
let closeTimer;
|
|
3056
|
+
const closeTimeout = new Promise((resolve) => {
|
|
3057
|
+
closeTimer = setTimeout(() => {
|
|
3058
|
+
traceDebug("export: browser_close did not finish within 20s; proceeding with HAR as-is");
|
|
3059
|
+
resolve();
|
|
3060
|
+
}, 2e4);
|
|
3061
|
+
});
|
|
3062
|
+
await Promise.race([closePromise, closeTimeout]);
|
|
3063
|
+
clearTimeout(closeTimer);
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* End the recording: the next one starts fresh. A successful export ends
|
|
3067
|
+
* the data-safety session too — the next recording's ledger must contain
|
|
3068
|
+
* only its own entries and blocks, and its teardown must not re-DELETE
|
|
3069
|
+
* this session's entities. The temp dir and HAR path stay stable so
|
|
3070
|
+
* recordHar stays in sync (SKYR-3830).
|
|
3071
|
+
*/
|
|
3072
|
+
_resetRecording() {
|
|
3073
|
+
this._trackedActions = [];
|
|
3074
|
+
this._droppedActions = [];
|
|
3075
|
+
const ra = this._browserBackend.context?.recorderActions;
|
|
3076
|
+
if (ra)
|
|
3077
|
+
ra.length = 0;
|
|
3078
|
+
this._recordingPhase.reset();
|
|
3079
|
+
this._passiveActionIds.clear();
|
|
3080
|
+
this._guard.resetSession();
|
|
3081
|
+
this._cleanupTempDir(true);
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* A recording whose resource provenance is invalid can never export: its
|
|
3085
|
+
* records are already deleted and the phases of its creates are fixed.
|
|
3086
|
+
* Close the browser (so the next HAR holds only the new attempt) and reset
|
|
3087
|
+
* the recording, so recording again from the start can export.
|
|
3088
|
+
*/
|
|
3089
|
+
async _refuseAndResetRecording(reason) {
|
|
3090
|
+
const created = this._guard.ledger.length;
|
|
3091
|
+
await this._closeBrowserForExport();
|
|
3092
|
+
this._resetRecording();
|
|
3093
|
+
const deleted = created ? ` Data safety teardown already deleted the ${created} record(s) this attempt created, setup data included.` : "";
|
|
3094
|
+
return {
|
|
3095
|
+
content: [{ type: "text", text: `### Error
|
|
3096
|
+
${reason}
|
|
3097
|
+
The recording was NOT exported, and it was reset.${deleted} The browser was closed. Record again from the start: browser_navigate, the setup steps, skyramp_finish_test_setup, then the scenario steps.` }],
|
|
3098
|
+
isError: true
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
/** See resolveSourceActions: a create's source action must have a JSONL line. */
|
|
3102
|
+
_resolveSourceActions() {
|
|
3103
|
+
for (const [entry, actionId] of (0, import_exportTool.resolveSourceActions)(this._trackedActions, this._guard.ledger, this._passiveActionIds))
|
|
3104
|
+
this._guard.setSourceAction(entry, actionId);
|
|
3105
|
+
}
|
|
3106
|
+
_maybeTrackAction(toolName, args, result, provenance, timestamp, pageAliasBeforeAction) {
|
|
2687
3107
|
if (result.isError) {
|
|
2688
3108
|
const errParsed = (0, import_response.parseResponse)(result);
|
|
2689
3109
|
const errText = (errParsed?.error ?? errParsed?.text ?? "unknown error").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
@@ -2699,14 +3119,14 @@ ${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFa
|
|
|
2699
3119
|
const popupAlias = this._pendingPopupAlias ?? void 0;
|
|
2700
3120
|
this._pendingPopupAlias = null;
|
|
2701
3121
|
const pageAlias = popupAlias ? pageAliasBeforeAction ?? this._currentPageAlias : this._currentPageAlias;
|
|
2702
|
-
this.
|
|
3122
|
+
this._recordAction({
|
|
2703
3123
|
toolName,
|
|
2704
3124
|
args,
|
|
2705
3125
|
code,
|
|
2706
3126
|
timestamp: timestamp ?? Date.now(),
|
|
2707
3127
|
pageAlias,
|
|
2708
3128
|
popupAlias
|
|
2709
|
-
});
|
|
3129
|
+
}, provenance);
|
|
2710
3130
|
traceDebug(`Tracked action: ${toolName} on ${pageAlias}${popupAlias ? ` \u2192 popup:${popupAlias}` : ""} (total: ${this._trackedActions.length})`);
|
|
2711
3131
|
}
|
|
2712
3132
|
}
|
|
@@ -2799,18 +3219,69 @@ ${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFa
|
|
|
2799
3219
|
}
|
|
2800
3220
|
}
|
|
2801
3221
|
async _autoExportAndClose() {
|
|
3222
|
+
let { teardownClean } = await this._session.settleAndTeardownBestEffort();
|
|
3223
|
+
const guardActive = this._session.active;
|
|
3224
|
+
const hasAudit = guardActive && (this._guard.ledger.length > 0 || this._guard.blocks.length > 0 || this._guard.unparsedCreates.length > 0);
|
|
3225
|
+
let zipWritten = false;
|
|
2802
3226
|
if (this._trackedActions.length) {
|
|
2803
3227
|
try {
|
|
2804
3228
|
this._consumeRecorderSelectors();
|
|
3229
|
+
this._resolveSourceActions();
|
|
3230
|
+
const provenance = (0, import_exportTool.validateResourceProvenance)(this._trackedActions, this._guard.ledger, this._recordingPhase.setupCompleted);
|
|
3231
|
+
if (!provenance.ok)
|
|
3232
|
+
process.stderr.write(`[skyramp] Auto-export: the recording's resource provenance is invalid, so test generation will refuse it: ${provenance.message}
|
|
3233
|
+
`);
|
|
2805
3234
|
const outputZip = import_path.default.resolve(this._outputDir, "skyramp_export.zip");
|
|
2806
|
-
const { jsonl: jsonlContent, actionCount } = (0, import_skyRampExport.buildJsonlContent)(this._trackedActions, "chromium", this._harPath);
|
|
2807
|
-
await (0, import_skyRampExport.writeSkyrampZip)(
|
|
2808
|
-
|
|
3235
|
+
const { jsonl: jsonlContent, actionCount } = (0, import_skyRampExport.buildJsonlContent)(this._trackedActions, "chromium", this._harPath, void 0, void 0, this._guard.ledger);
|
|
3236
|
+
await (0, import_skyRampExport.writeSkyrampZip)(
|
|
3237
|
+
outputZip,
|
|
3238
|
+
jsonlContent,
|
|
3239
|
+
this._harPath,
|
|
3240
|
+
guardActive ? [{ name: "dataSafetyLedger.json", content: this._guard.ledgerJson(this._recordingPhase.setupCompleted) }] : void 0
|
|
3241
|
+
);
|
|
3242
|
+
zipWritten = true;
|
|
3243
|
+
const incompleteReasons = [];
|
|
3244
|
+
if (!teardownClean)
|
|
3245
|
+
incompleteReasons.push("teardown left entities behind \u2014 see dataSafetyLedger.json");
|
|
3246
|
+
const unparsedCount = this._guard.unparsedCreates.length;
|
|
3247
|
+
if (unparsedCount > 0)
|
|
3248
|
+
incompleteReasons.push(`${unparsedCount} unidentified create(s) \u2014 see dataSafetyLedger.json`);
|
|
3249
|
+
const incompleteNote = incompleteReasons.length ? ` (data-safety-incomplete: ${incompleteReasons.join("; ")})` : "";
|
|
3250
|
+
process.stderr.write(`[skyramp] Auto-exported ${actionCount} actions to ${outputZip}${incompleteNote}
|
|
3251
|
+
`);
|
|
3252
|
+
} catch (e) {
|
|
3253
|
+
process.stderr.write(`[skyramp] Auto-export failed: ${String(e)}
|
|
2809
3254
|
`);
|
|
2810
|
-
} catch {
|
|
2811
3255
|
}
|
|
2812
3256
|
}
|
|
2813
3257
|
this._browserBackend?.serverClosed();
|
|
3258
|
+
if (guardActive) {
|
|
3259
|
+
const late = await this._session.settleBodyReads();
|
|
3260
|
+
if (late.timedOut || this._guard.pendingTeardownCount > 0)
|
|
3261
|
+
teardownClean = false;
|
|
3262
|
+
}
|
|
3263
|
+
const mustPreserve = !teardownClean || hasAudit && !zipWritten;
|
|
3264
|
+
const callerLedger = guardActive && this._guard.config.ledgerDir && (hasAudit || import_fs.default.existsSync(this._guard.ledgerPath)) ? this._guard.ledgerPath : void 0;
|
|
3265
|
+
let preserve = "not-needed";
|
|
3266
|
+
if (mustPreserve || callerLedger) {
|
|
3267
|
+
try {
|
|
3268
|
+
const preservedPath = callerLedger ?? import_path.default.join(this._outputDir, `dataSafetyLedger-${Date.now()}.json`);
|
|
3269
|
+
(0, import_dataSafetyGuard.writeLedgerFile)(preservedPath, this._guard.ledgerJson(this._recordingPhase.setupCompleted));
|
|
3270
|
+
if (mustPreserve)
|
|
3271
|
+
process.stderr.write(`[skyramp] Data safety: ${teardownClean ? "audit trail" : "teardown incomplete"} \u2014 ledger preserved at ${preservedPath}
|
|
3272
|
+
`);
|
|
3273
|
+
preserve = "done";
|
|
3274
|
+
} catch (e) {
|
|
3275
|
+
process.stderr.write(`[skyramp] Data safety: failed to preserve ledger (${String(e)}) \u2014 dumping inline so the record survives:
|
|
3276
|
+
${this._guard.ledgerJson(this._recordingPhase.setupCompleted)}
|
|
3277
|
+
`);
|
|
3278
|
+
preserve = "failed";
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
traceDebug(`data safety abnormal-end preserve outcome: ${preserve}`);
|
|
3282
|
+
this._recordingPhase.reset();
|
|
3283
|
+
this._passiveActionIds.clear();
|
|
3284
|
+
this._guard.resetSession();
|
|
2814
3285
|
this._cleanupTempDir(false);
|
|
2815
3286
|
}
|
|
2816
3287
|
}
|