@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
package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts
CHANGED
|
@@ -29,9 +29,15 @@ import { generateCode } from '../codegen/language';
|
|
|
29
29
|
import { Recorder, RecorderEvent } from '../recorder';
|
|
30
30
|
import { BrowserContext } from '../browserContext';
|
|
31
31
|
import { JsonlReader } from '../codegen/skyramp/jsonlReader';
|
|
32
|
-
import { ReplayEngine,
|
|
32
|
+
import { ReplayEngine, UrlPatternCondition } from './skyramp/replayEngine';
|
|
33
|
+
import { AliasRemapper } from './skyramp/aliasRemapper';
|
|
33
34
|
import type { ReplayEngineOptions } from './skyramp/replayEngine';
|
|
34
35
|
|
|
36
|
+
// Options for loading a trace and replaying a (sub-)range of it. startStep and
|
|
37
|
+
// stopStep are 1-based indices into the original trace (what the step picker
|
|
38
|
+
// shows); they slice the range that gets appended at the live recording cursor.
|
|
39
|
+
type LoadTraceOptions = ReplayEngineOptions & { startStep?: number; stopStep?: number };
|
|
40
|
+
|
|
35
41
|
import type { Page } from '../page';
|
|
36
42
|
import type * as actions from '@recorder/actions';
|
|
37
43
|
import type { CallLog, ElementInfo, Mode, Source } from '@recorder/recorderTypes';
|
|
@@ -134,11 +140,16 @@ export class RecorderApp {
|
|
|
134
140
|
const deleted = this._actions[lastRealIndex];
|
|
135
141
|
const { name, signals: _signals, ariaSnapshot: _aria, ...rest } = deleted.action as any;
|
|
136
142
|
const summary: Record<string, unknown> = { name };
|
|
137
|
-
if (rest.selector)
|
|
138
|
-
|
|
139
|
-
if (rest.
|
|
140
|
-
|
|
141
|
-
if (rest.
|
|
143
|
+
if (rest.selector)
|
|
144
|
+
summary['selector'] = rest.selector;
|
|
145
|
+
if (rest.url)
|
|
146
|
+
summary['url'] = rest.url;
|
|
147
|
+
if (rest.key)
|
|
148
|
+
summary['key'] = rest.key;
|
|
149
|
+
if (rest.text)
|
|
150
|
+
summary['text'] = rest.text;
|
|
151
|
+
if (rest.timestamp)
|
|
152
|
+
summary['timestamp'] = rest.timestamp;
|
|
142
153
|
const commentAction: actions.ActionInContext = {
|
|
143
154
|
frame: deleted.frame,
|
|
144
155
|
startTime: deleted.startTime,
|
|
@@ -202,14 +213,35 @@ export class RecorderApp {
|
|
|
202
213
|
this._recorder.executeScript(data.params.script);
|
|
203
214
|
return;
|
|
204
215
|
}
|
|
216
|
+
if (data.event === 'parseTrace') {
|
|
217
|
+
// Parse the loaded trace and push per-step previews (jsonl + ts + python)
|
|
218
|
+
// back to the step picker. Parsing goes through the same JsonlReader the
|
|
219
|
+
// replay uses, so the step indices align with the array we later slice.
|
|
220
|
+
const loadedActions = JsonlReader._parseContent(data.params.jsonlContent);
|
|
221
|
+
const steps = this._buildTraceSteps(loadedActions);
|
|
222
|
+
// Echo the request token so the UI can drop a stale response (e.g. if the
|
|
223
|
+
// user cancelled and loaded a different trace while this one was parsing).
|
|
224
|
+
this._page.mainFrame().evaluateExpression(
|
|
225
|
+
((p: { steps: any[], token: number }) => { window.playwrightSetTraceSteps?.(p.steps, p.token); }).toString(),
|
|
226
|
+
{ isFunction: true },
|
|
227
|
+
{ steps, token: data.params.token },
|
|
228
|
+
).catch(() => { });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
205
231
|
if (data.event === 'loadTrace') {
|
|
232
|
+
const opts: LoadTraceOptions = {
|
|
233
|
+
startStep: data.params.startStep,
|
|
234
|
+
stopStep: data.params.stopStep,
|
|
235
|
+
stopCondition: data.params.stopUrl !== undefined ? new UrlPatternCondition(data.params.stopUrl) : undefined,
|
|
236
|
+
delayBetweenActions: data.params.delayBetweenActions,
|
|
237
|
+
};
|
|
206
238
|
if (data.params.jsonlContent !== undefined) {
|
|
207
239
|
// Content sent directly from the browser file picker (JSONL/TXT, or extracted from ZIP).
|
|
208
240
|
const loadedActions = JsonlReader._parseContent(data.params.jsonlContent);
|
|
209
|
-
void this._runReplay(loadedActions,
|
|
241
|
+
void this._runReplay(loadedActions, opts);
|
|
210
242
|
} else {
|
|
211
243
|
// File path provided (e.g. from CLI flag).
|
|
212
|
-
void this.loadTrace(data.params.path,
|
|
244
|
+
void this.loadTrace(data.params.path, opts);
|
|
213
245
|
}
|
|
214
246
|
return;
|
|
215
247
|
}
|
|
@@ -380,9 +412,9 @@ export class RecorderApp {
|
|
|
380
412
|
|
|
381
413
|
// Write JSON with pretty formatting
|
|
382
414
|
fs.writeFileSync(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
415
|
+
filepath,
|
|
416
|
+
JSON.stringify(action.snapshotData, null, 2),
|
|
417
|
+
'utf-8'
|
|
386
418
|
);
|
|
387
419
|
|
|
388
420
|
// console.log(`[RecorderApp] DOM snapshot saved successfully to: ${filepath}`);
|
|
@@ -440,39 +472,69 @@ export class RecorderApp {
|
|
|
440
472
|
}).toString(), { isFunction: true }, { sourceId }).catch(() => { });
|
|
441
473
|
}
|
|
442
474
|
|
|
443
|
-
async loadTrace(zipOrJsonlPath: string, opts:
|
|
475
|
+
async loadTrace(zipOrJsonlPath: string, opts: LoadTraceOptions = {}) {
|
|
444
476
|
const loadedActions = zipOrJsonlPath.endsWith('.zip')
|
|
445
477
|
? await JsonlReader.fromZip(zipOrJsonlPath)
|
|
446
478
|
: JsonlReader.fromFile(zipOrJsonlPath);
|
|
447
479
|
await this._runReplay(loadedActions, opts);
|
|
448
480
|
}
|
|
449
481
|
|
|
450
|
-
private async _runReplay(loadedActions: actions.ActionInContext[], opts:
|
|
482
|
+
private async _runReplay(loadedActions: actions.ActionInContext[], opts: LoadTraceOptions = {}) {
|
|
451
483
|
if (!loadedActions.length)
|
|
452
484
|
return;
|
|
453
485
|
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
486
|
+
// Slice the trace to the chosen [startStep, stopStep] range (1-based, inclusive).
|
|
487
|
+
// startStep lets the user skip a stale prefix (e.g. an old login page) and
|
|
488
|
+
// append only the still-valid tail onto the current recording.
|
|
489
|
+
const startIndex = opts.startStep ? Math.max(0, opts.startStep - 1) : 0;
|
|
490
|
+
const endIndex = opts.stopStep ? Math.min(loadedActions.length, opts.stopStep) : loadedActions.length;
|
|
491
|
+
const replayActions = loadedActions.slice(startIndex, endIndex);
|
|
492
|
+
if (!replayActions.length) {
|
|
493
|
+
// Out-of-range or start>stop produced an empty range. The picker UI clamps
|
|
494
|
+
// these, so this only arises from a bad programmatic/CLI call — surface it
|
|
495
|
+
// rather than silently doing nothing.
|
|
496
|
+
this._page.mainFrame().evaluateExpression(
|
|
497
|
+
((msg: string) => { window.playwrightSetReplayError?.(msg); }).toString(),
|
|
498
|
+
{ isFunction: true },
|
|
499
|
+
`Nothing to replay: the selected step range is empty (trace has ${loadedActions.length} steps; start=${opts.startStep ?? 1}, stop=${opts.stopStep ?? loadedActions.length}).`,
|
|
500
|
+
).catch(() => { });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Remap the fragment's page aliases (page/page1/page2...) onto the live
|
|
505
|
+
// session so a multi-tab fragment can be appended mid-recording. Seed from
|
|
506
|
+
// the last already-recorded action's page alias — the best available proxy
|
|
507
|
+
// for where recording left off (it is a live alias, recorded this session) —
|
|
508
|
+
// or 'page' from an empty start. NOTE: this is the last *recorded* tab, which
|
|
509
|
+
// may differ from the visually-focused tab if the user switched tabs without
|
|
510
|
+
// recording an action there; the manual recorder exposes no focused-tab signal.
|
|
511
|
+
const seedLiveAlias = this._actions.length ? this._actions[this._actions.length - 1].frame.pageAlias : 'page';
|
|
512
|
+
const aliasRemapper = new AliasRemapper(this._recorder.pageAliases(), replayActions, seedLiveAlias);
|
|
513
|
+
|
|
514
|
+
// Append onto the existing recording (no wipe): replayed actions are pushed
|
|
515
|
+
// after whatever the user has already recorded, then they continue from here.
|
|
516
|
+
// From an empty recording this is identical to a clean load.
|
|
458
517
|
this._recorder.setMode('replaying');
|
|
459
518
|
|
|
519
|
+
const total = replayActions.length;
|
|
460
520
|
const engine = new ReplayEngine(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
521
|
+
this._recorder.pageAliases(),
|
|
522
|
+
replayActions,
|
|
523
|
+
(action, index) => {
|
|
524
|
+
this._actions.push(action);
|
|
525
|
+
this._updateActions('reveal');
|
|
526
|
+
// Notify the UI of replay progress.
|
|
527
|
+
this._page.mainFrame().evaluateExpression(
|
|
528
|
+
((p: { completed: number, total: number }) => {
|
|
529
|
+
window.playwrightSetReplayProgress?.(p.completed, p.total);
|
|
530
|
+
}).toString(),
|
|
531
|
+
{ isFunction: true },
|
|
532
|
+
{ completed: index + 1, total },
|
|
533
|
+
).catch(() => { });
|
|
534
|
+
},
|
|
535
|
+
// Pass only ReplayEngineOptions fields — opts is LoadTraceOptions, whose
|
|
536
|
+
// startStep/stopStep have already been applied via the slice above.
|
|
537
|
+
{ onProgress: opts.onProgress, stopCondition: opts.stopCondition, delayBetweenActions: opts.delayBetweenActions, aliasRemapper },
|
|
476
538
|
);
|
|
477
539
|
this._replayEngine = engine;
|
|
478
540
|
|
|
@@ -482,9 +544,9 @@ export class RecorderApp {
|
|
|
482
544
|
const { message, actionName, stepIndex } = result.error;
|
|
483
545
|
const errorMsg = `Replay stopped at step ${stepIndex} (${actionName}): ${message}\n\nYou can continue recording from this point.`;
|
|
484
546
|
this._page.mainFrame().evaluateExpression(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
547
|
+
((msg: string) => { window.playwrightSetReplayError?.(msg); }).toString(),
|
|
548
|
+
{ isFunction: true },
|
|
549
|
+
errorMsg,
|
|
488
550
|
).catch(() => { });
|
|
489
551
|
}
|
|
490
552
|
} finally {
|
|
@@ -493,6 +555,38 @@ export class RecorderApp {
|
|
|
493
555
|
}
|
|
494
556
|
}
|
|
495
557
|
|
|
558
|
+
// Build per-step previews for the load-trace step picker. Each step carries
|
|
559
|
+
// the source line in jsonl/ts/python so the picker can toggle languages. The
|
|
560
|
+
// array is index-aligned with the parsed actions (one entry per action, even
|
|
561
|
+
// if a generator emits no text), so picker indices map 1:1 to replay slicing.
|
|
562
|
+
private _buildTraceSteps(loadedActions: actions.ActionInContext[]) {
|
|
563
|
+
const wanted = [
|
|
564
|
+
{ key: 'jsonl', id: 'jsonl' },
|
|
565
|
+
{ key: 'ts', id: 'playwright-test' },
|
|
566
|
+
{ key: 'python', id: 'python' },
|
|
567
|
+
] as const;
|
|
568
|
+
const generators = wanted.map(w => ({ key: w.key, gen: [...languageSet()].find(g => g.id === w.id) }));
|
|
569
|
+
|
|
570
|
+
return loadedActions.map((action, i) => {
|
|
571
|
+
const code: Record<string, string> = {};
|
|
572
|
+
for (const { key, gen } of generators)
|
|
573
|
+
code[key] = (gen?.generateAction(action) || '').trim();
|
|
574
|
+
// Tabs this action opens (popup signals). The picker uses these to detect,
|
|
575
|
+
// as the user drags the range, whether a step targets a tab the range never
|
|
576
|
+
// opens (its opener step was skipped) - which would fail at replay time.
|
|
577
|
+
const opensAliases = action.action.signals
|
|
578
|
+
.filter((s): s is actions.PopupSignal => s.name === 'popup')
|
|
579
|
+
.map(s => s.popupAlias);
|
|
580
|
+
return {
|
|
581
|
+
index: i + 1,
|
|
582
|
+
name: action.action.name,
|
|
583
|
+
pageAlias: action.frame.pageAlias,
|
|
584
|
+
opensAliases,
|
|
585
|
+
code,
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
496
590
|
private _updateActions(reveal?: 'reveal') {
|
|
497
591
|
const recorderSources = [];
|
|
498
592
|
const actions = collapseActions(this._actions);
|
|
@@ -571,12 +665,4 @@ function findPageByGuid(context: BrowserContext, guid: string) {
|
|
|
571
665
|
return context.pages().find(p => p.guid === guid);
|
|
572
666
|
}
|
|
573
667
|
|
|
574
|
-
function _buildStopCondition(params: any) {
|
|
575
|
-
if (params?.stopStep !== undefined)
|
|
576
|
-
return new StepNumberCondition(params.stopStep);
|
|
577
|
-
if (params?.stopUrl !== undefined)
|
|
578
|
-
return new UrlPatternCondition(params.stopUrl);
|
|
579
|
-
return undefined;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
668
|
const recorderAppSymbol = Symbol('recorderApp');
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Page } from '../../page';
|
|
18
|
+
import type * as actions from '@recorder/actions';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Remaps the page aliases of a loaded trace fragment onto the live recording
|
|
22
|
+
* session, so a multi-tab fragment can be replayed and appended mid-recording.
|
|
23
|
+
*
|
|
24
|
+
* The problem: a fragment's actions carry the aliases they were recorded with
|
|
25
|
+
* (page, page1, page2...). Replay resolves those against the recorder's LIVE
|
|
26
|
+
* `_pageAliases` map. On a clean load this lines up by construction (replay
|
|
27
|
+
* starts blank, so the live alias counter advances in lockstep with the trace).
|
|
28
|
+
* Mid-recording the live session may already have tabs open, so the fragment's
|
|
29
|
+
* aliases collide with the live ones.
|
|
30
|
+
*
|
|
31
|
+
* The fix: bind fragment aliases to live aliases lazily.
|
|
32
|
+
* - Seed: the fragment's primary alias (its first action's page) maps to the
|
|
33
|
+
* tab the user is currently on.
|
|
34
|
+
* - Popups: when a replayed action opens a popup (carries a `popup` signal),
|
|
35
|
+
* the recorder assigns the new tab a fresh live alias; we bind the
|
|
36
|
+
* fragment's popup alias to it and rewrite the signal so generated code is
|
|
37
|
+
* coherent.
|
|
38
|
+
*
|
|
39
|
+
* Both `frame.pageAlias` (what tab an action targets) and popup signal
|
|
40
|
+
* `popupAlias` (what tab an action opens) are rewritten in place on the action
|
|
41
|
+
* objects, which are the same objects fed into codegen.
|
|
42
|
+
*/
|
|
43
|
+
export class AliasRemapper {
|
|
44
|
+
private readonly _pageAliases: Map<Page, string>;
|
|
45
|
+
// fragment alias -> live alias
|
|
46
|
+
private readonly _bindings = new Map<string, string>();
|
|
47
|
+
// live aliases already claimed by a fragment alias (so a later popup doesn't reuse one)
|
|
48
|
+
private readonly _claimedLiveAliases = new Set<string>();
|
|
49
|
+
// Live aliases present right before the current action executes; used to spot
|
|
50
|
+
// the fresh alias a popup-opening action creates.
|
|
51
|
+
private _liveBefore = new Set<string>();
|
|
52
|
+
|
|
53
|
+
constructor(pageAliases: Map<Page, string>, fragmentActions: actions.ActionInContext[], seedLiveAlias: string) {
|
|
54
|
+
this._pageAliases = pageAliases;
|
|
55
|
+
// Seed: the fragment's primary alias is whatever its first action targets.
|
|
56
|
+
const primaryFragmentAlias = fragmentActions[0]?.frame.pageAlias ?? 'page';
|
|
57
|
+
// Guard against a stale seed: if the last-recorded tab was closed,
|
|
58
|
+
// seedLiveAlias may no longer exist live, which would fail the first action
|
|
59
|
+
// with "page not found". Fall back to the main 'page' tab, else any live tab.
|
|
60
|
+
const liveAliases = new Set(pageAliases.values());
|
|
61
|
+
const seed = liveAliases.has(seedLiveAlias)
|
|
62
|
+
? seedLiveAlias
|
|
63
|
+
: (liveAliases.has('page') ? 'page' : [...liveAliases][0]);
|
|
64
|
+
if (seed) {
|
|
65
|
+
this._bindings.set(primaryFragmentAlias, seed);
|
|
66
|
+
this._claimedLiveAliases.add(seed);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Rewrite the action's target tab to a live alias before it executes. */
|
|
71
|
+
beforeAction(action: actions.ActionInContext): void {
|
|
72
|
+
this._liveBefore = new Set(this._pageAliases.values());
|
|
73
|
+
const live = this._bindings.get(action.frame.pageAlias);
|
|
74
|
+
if (!live) {
|
|
75
|
+
// The action targets a tab whose opener was not in the replayed range
|
|
76
|
+
// (e.g. it was in a skipped prefix). We cannot reconstruct it.
|
|
77
|
+
throw new Error(`Cannot replay fragment: it acts on tab '${action.frame.pageAlias}' but the step that opened that tab is outside the selected range. Include the step that opens it, or choose a range that stays on one tab.`);
|
|
78
|
+
}
|
|
79
|
+
action.frame.pageAlias = live;
|
|
80
|
+
// Keep pageGuid consistent with the remapped alias: the JSONL export spreads
|
|
81
|
+
// the whole frame (incl. pageGuid), so a stale trace guid would leak into the
|
|
82
|
+
// combined recording. Point it at the live page now bound to this alias.
|
|
83
|
+
const livePage = [...this._pageAliases.entries()].find(([, a]) => a === live)?.[0];
|
|
84
|
+
if (livePage)
|
|
85
|
+
action.frame.pageGuid = livePage.guid;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* After the action executes, bind any popup it opened and rewrite the popup
|
|
90
|
+
* signal's alias to the live one. Waits briefly for the new tab to register.
|
|
91
|
+
*/
|
|
92
|
+
async afterAction(action: actions.ActionInContext): Promise<void> {
|
|
93
|
+
for (const signal of action.action.signals) {
|
|
94
|
+
if (signal.name !== 'popup')
|
|
95
|
+
continue;
|
|
96
|
+
const fresh = await this._waitForFreshLiveAlias();
|
|
97
|
+
if (!fresh)
|
|
98
|
+
continue;
|
|
99
|
+
this._bindings.set(signal.popupAlias, fresh);
|
|
100
|
+
this._claimedLiveAliases.add(fresh);
|
|
101
|
+
signal.popupAlias = fresh;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Poll the live alias map for a tab that appeared since beforeAction and is unclaimed. */
|
|
106
|
+
private async _waitForFreshLiveAlias(): Promise<string | undefined> {
|
|
107
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
108
|
+
const fresh = [...this._pageAliases.values()].find(a => !this._liveBefore.has(a) && !this._claimedLiveAliases.has(a));
|
|
109
|
+
if (fresh)
|
|
110
|
+
return fresh;
|
|
111
|
+
await new Promise<void>(resolve => setTimeout(resolve, 50));
|
|
112
|
+
}
|
|
113
|
+
// The expected popup never opened. Return undefined rather than guessing an
|
|
114
|
+
// unrelated unclaimed tab — binding to the wrong page would silently run
|
|
115
|
+
// later actions against it. Leaving the alias unbound makes the next action
|
|
116
|
+
// on it fail deterministically via beforeAction's guard.
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { urlMatches } from '../../../utils/isomorphic/urlMatch';
|
|
18
18
|
import { performAction } from '../recorderRunner';
|
|
19
19
|
|
|
20
|
+
import type { AliasRemapper } from './aliasRemapper';
|
|
20
21
|
import type { Page } from '../../page';
|
|
21
22
|
import type * as actions from '@recorder/actions';
|
|
22
23
|
|
|
@@ -81,6 +82,8 @@ export type ReplayEngineOptions = {
|
|
|
81
82
|
stopCondition?: StopCondition;
|
|
82
83
|
/** Delay in ms between actions. Default: 0 (no artificial delay). */
|
|
83
84
|
delayBetweenActions?: number;
|
|
85
|
+
/** Optional page-alias remapper for replaying a fragment mid-recording (multi-tab). */
|
|
86
|
+
aliasRemapper?: AliasRemapper;
|
|
84
87
|
};
|
|
85
88
|
|
|
86
89
|
export class ReplayEngine {
|
|
@@ -95,10 +98,10 @@ export class ReplayEngine {
|
|
|
95
98
|
delayBetweenActions: number;
|
|
96
99
|
|
|
97
100
|
constructor(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
pageAliases: Map<Page, string>,
|
|
102
|
+
actions: actions.ActionInContext[],
|
|
103
|
+
onActionCompleted: (action: actions.ActionInContext, index: number) => void,
|
|
104
|
+
opts: ReplayEngineOptions = {},
|
|
102
105
|
) {
|
|
103
106
|
this._pageAliases = pageAliases;
|
|
104
107
|
this._actions = actions;
|
|
@@ -128,10 +131,25 @@ export class ReplayEngine {
|
|
|
128
131
|
action.startTime = replayStart;
|
|
129
132
|
(action.action as any).timestamp = replayStart.toString();
|
|
130
133
|
|
|
134
|
+
// Remap the action's target tab onto the live session before executing.
|
|
135
|
+
// Done for every action (incl. metadata-only ones like openPage) so the
|
|
136
|
+
// appended action objects carry live aliases for coherent codegen.
|
|
137
|
+
try {
|
|
138
|
+
this._opts.aliasRemapper?.beforeAction(action);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return {
|
|
141
|
+
completedCount,
|
|
142
|
+
stopReason: 'error',
|
|
143
|
+
error: { message: (e as Error).message, actionName: action.action.name, stepIndex: i + 1 },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
if (!_METADATA_ONLY_ACTIONS.has(action.action.name)) {
|
|
132
148
|
try {
|
|
133
149
|
await performAction(this._pageAliases, action, 30000);
|
|
134
150
|
action.endTime = Date.now();
|
|
151
|
+
// Bind any popup this action opened to its fresh live alias.
|
|
152
|
+
await this._opts.aliasRemapper?.afterAction(action);
|
|
135
153
|
} catch (e) {
|
|
136
154
|
// Stop replay and surface the error so the user can take over.
|
|
137
155
|
return {
|
|
@@ -156,8 +174,10 @@ export class ReplayEngine {
|
|
|
156
174
|
});
|
|
157
175
|
|
|
158
176
|
// Check stop condition AFTER executing so URL-based conditions fire
|
|
159
|
-
// right after the navigate that brought us to the target page.
|
|
160
|
-
|
|
177
|
+
// right after the navigate that brought us to the target page. Use the
|
|
178
|
+
// tab this action ran on (its alias is already remapped to a live alias),
|
|
179
|
+
// so URL stops work even when a fragment is remapped onto a non-'page' tab.
|
|
180
|
+
const currentUrl = this._currentUrl(action.frame.pageAlias);
|
|
161
181
|
if (this._opts.stopCondition?.shouldStop(action, i, currentUrl))
|
|
162
182
|
return { completedCount, stopReason: 'condition' };
|
|
163
183
|
|
|
@@ -176,9 +196,12 @@ export class ReplayEngine {
|
|
|
176
196
|
return { completedCount, stopReason: 'done' };
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
private _currentUrl(): string {
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
private _currentUrl(alias: string): string {
|
|
200
|
+
// URL of the tab the current action ran on; fall back to the main 'page'
|
|
201
|
+
// tab if that alias can't be resolved (e.g. it was closed).
|
|
202
|
+
const pages = [...this._pageAliases.keys()];
|
|
203
|
+
const page = pages.find(p => this._pageAliases.get(p) === alias)
|
|
204
|
+
?? pages.find(p => this._pageAliases.get(p) === 'page');
|
|
205
|
+
return page?.mainFrame().url() ?? '';
|
|
183
206
|
}
|
|
184
207
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./build/index.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./build/workspace/index.d.ts",
|
|
11
11
|
"default": "./build/workspace/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./types": {
|
|
14
|
+
"types": "./build/types/index.d.ts",
|
|
15
|
+
"default": "./build/types/index.js"
|
|
16
|
+
},
|
|
13
17
|
"./build/*": "./build/*"
|
|
14
18
|
},
|
|
15
19
|
"type": "module",
|
|
@@ -17,9 +21,12 @@
|
|
|
17
21
|
"mcp": "./build/index.js"
|
|
18
22
|
},
|
|
19
23
|
"scripts": {
|
|
24
|
+
"prepare": "npm run build",
|
|
20
25
|
"clean-build": "rm -rf build && npm run build",
|
|
21
26
|
"build": "tsc && chmod 755 build/index.js",
|
|
22
27
|
"build:prod": "tsc --sourceMap false && chmod 755 build/index.js",
|
|
28
|
+
"swap-core": "node scripts/swap-modified-core.mjs",
|
|
29
|
+
"prepack": "node scripts/swap-modified-core.mjs",
|
|
23
30
|
"pack": "npm run build:prod && npm pack",
|
|
24
31
|
"start": "node build/index.js",
|
|
25
32
|
"test:startup": "time node build/index.js --help",
|
|
@@ -60,11 +67,11 @@
|
|
|
60
67
|
"dependencies": {
|
|
61
68
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
62
69
|
"@playwright/test": "^1.55.0",
|
|
63
|
-
"@skyramp/skyramp": "1.3.
|
|
70
|
+
"@skyramp/skyramp": "1.3.29",
|
|
64
71
|
"dockerode": "^5.0.0",
|
|
65
72
|
"fast-glob": "^3.3.3",
|
|
66
73
|
"js-yaml": "^4.1.1",
|
|
67
|
-
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.
|
|
74
|
+
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.10.0.tgz",
|
|
68
75
|
"simple-git": "^3.30.0",
|
|
69
76
|
"typescript": "^5.8.3",
|
|
70
77
|
"zod": "^3.25.3"
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var which = require("../")
|
|
3
|
-
if (process.argv.length < 3)
|
|
4
|
-
usage()
|
|
5
|
-
|
|
6
|
-
function usage () {
|
|
7
|
-
console.error('usage: which [-as] program ...')
|
|
8
|
-
process.exit(1)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
var all = false
|
|
12
|
-
var silent = false
|
|
13
|
-
var dashdash = false
|
|
14
|
-
var args = process.argv.slice(2).filter(function (arg) {
|
|
15
|
-
if (dashdash || !/^-/.test(arg))
|
|
16
|
-
return true
|
|
17
|
-
|
|
18
|
-
if (arg === '--') {
|
|
19
|
-
dashdash = true
|
|
20
|
-
return false
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
var flags = arg.substr(1).split('')
|
|
24
|
-
for (var f = 0; f < flags.length; f++) {
|
|
25
|
-
var flag = flags[f]
|
|
26
|
-
switch (flag) {
|
|
27
|
-
case 's':
|
|
28
|
-
silent = true
|
|
29
|
-
break
|
|
30
|
-
case 'a':
|
|
31
|
-
all = true
|
|
32
|
-
break
|
|
33
|
-
default:
|
|
34
|
-
console.error('which: illegal option -- ' + flag)
|
|
35
|
-
usage()
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return false
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
process.exit(args.reduce(function (pv, current) {
|
|
42
|
-
try {
|
|
43
|
-
var f = which.sync(current, { all: all })
|
|
44
|
-
if (all)
|
|
45
|
-
f = f.join('\n')
|
|
46
|
-
if (!silent)
|
|
47
|
-
console.log(f)
|
|
48
|
-
return pv;
|
|
49
|
-
} catch (e) {
|
|
50
|
-
return 1;
|
|
51
|
-
}
|
|
52
|
-
}, 0))
|
package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
process.title = 'mime';
|
|
6
|
-
let mime = require('.');
|
|
7
|
-
let pkg = require('./package.json');
|
|
8
|
-
let args = process.argv.splice(2);
|
|
9
|
-
|
|
10
|
-
if (args.includes('--version') || args.includes('-v') || args.includes('--v')) {
|
|
11
|
-
console.log(pkg.version);
|
|
12
|
-
process.exit(0);
|
|
13
|
-
} else if (args.includes('--name') || args.includes('-n') || args.includes('--n')) {
|
|
14
|
-
console.log(pkg.name);
|
|
15
|
-
process.exit(0);
|
|
16
|
-
} else if (args.includes('--help') || args.includes('-h') || args.includes('--h')) {
|
|
17
|
-
console.log(pkg.name + ' - ' + pkg.description + '\n');
|
|
18
|
-
console.log(`Usage:
|
|
19
|
-
|
|
20
|
-
mime [flags] [path_or_extension]
|
|
21
|
-
|
|
22
|
-
Flags:
|
|
23
|
-
--help, -h Show this message
|
|
24
|
-
--version, -v Display the version
|
|
25
|
-
--name, -n Print the name of the program
|
|
26
|
-
|
|
27
|
-
Note: the command will exit after it executes if a command is specified
|
|
28
|
-
The path_or_extension is the path to the file or the extension of the file.
|
|
29
|
-
|
|
30
|
-
Examples:
|
|
31
|
-
mime --help
|
|
32
|
-
mime --version
|
|
33
|
-
mime --name
|
|
34
|
-
mime -v
|
|
35
|
-
mime src/log.js
|
|
36
|
-
mime new.py
|
|
37
|
-
mime foo.sh
|
|
38
|
-
`);
|
|
39
|
-
process.exit(0);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let file = args[0];
|
|
43
|
-
let type = mime.getType(file);
|
|
44
|
-
|
|
45
|
-
process.stdout.write(type + '\n');
|
|
46
|
-
|
package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { UserError, cli, help } from './dist/cli.mjs'
|
|
4
|
-
|
|
5
|
-
cli(process.stdin, error => {
|
|
6
|
-
if (error instanceof UserError) {
|
|
7
|
-
if (error.code === UserError.ARGS) console.error(`${help}\n`)
|
|
8
|
-
console.error(error.message)
|
|
9
|
-
process.exitCode = error.code
|
|
10
|
-
} else if (error) throw error
|
|
11
|
-
})
|