@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.
Files changed (90) hide show
  1. package/build/index.js +5 -2
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
  3. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
  4. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
  5. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
  7. package/build/prompts/sut-setup/shared.d.ts +39 -0
  8. package/build/prompts/sut-setup/shared.js +132 -0
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
  12. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
  13. package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
  14. package/build/prompts/testbot/testbot-prompts.js +113 -22
  15. package/build/prompts/testbot/testbot-prompts.test.js +95 -45
  16. package/build/resources/sutSetupResource.d.ts +2 -0
  17. package/build/resources/sutSetupResource.js +45 -0
  18. package/build/resources/testbotResource.js +2 -2
  19. package/build/services/TestDiscoveryService.d.ts +2 -2
  20. package/build/services/TestDiscoveryService.js +22 -12
  21. package/build/services/TestExecutionService.d.ts +10 -1
  22. package/build/services/TestExecutionService.js +158 -26
  23. package/build/services/TestExecutionService.test.js +306 -0
  24. package/build/tools/executeSkyrampTestTool.js +26 -7
  25. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  26. package/build/tools/submitReportTool.d.ts +10 -0
  27. package/build/tools/submitReportTool.js +106 -39
  28. package/build/tools/submitReportTool.test.js +296 -50
  29. package/build/tools/test-management/actionsTool.js +21 -7
  30. package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
  31. package/build/tools/test-management/analyzeChangesTool.js +106 -41
  32. package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
  33. package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
  34. package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
  35. package/build/tools/workspace/serviceUpsert.d.ts +12 -0
  36. package/build/tools/workspace/serviceUpsert.js +23 -0
  37. package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
  38. package/build/tools/workspace/serviceUpsert.test.js +50 -0
  39. package/build/types/TestAnalysis.d.ts +2 -1
  40. package/build/types/TestExecution.d.ts +12 -2
  41. package/build/types/TestExecution.js +11 -1
  42. package/build/types/TestbotReport.d.ts +68 -0
  43. package/build/types/TestbotReport.js +1 -0
  44. package/build/types/index.d.ts +4 -0
  45. package/build/types/index.js +3 -0
  46. package/build/utils/AnalysisStateManager.d.ts +61 -1
  47. package/build/utils/AnalysisStateManager.js +144 -3
  48. package/build/utils/AnalysisStateManager.test.js +98 -0
  49. package/build/utils/docker.test.js +1 -1
  50. package/build/utils/routeParsers.d.ts +0 -10
  51. package/build/utils/routeParsers.js +31 -38
  52. package/build/utils/scenarioDrafting.d.ts +1 -1
  53. package/build/utils/scenarioDrafting.js +23 -1
  54. package/build/utils/utils.d.ts +7 -1
  55. package/build/utils/utils.js +10 -0
  56. package/build/utils/versions.d.ts +3 -3
  57. package/build/utils/versions.js +1 -1
  58. package/build/workspace/workspace.d.ts +10 -10
  59. package/build/workspace/workspace.js +7 -7
  60. package/build/workspace/workspace.test.js +6 -6
  61. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
  62. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
  63. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  64. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  65. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  66. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
  67. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
  68. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
  69. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
  70. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
  71. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  72. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
  73. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
  74. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
  75. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  76. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  77. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
  78. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  79. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  80. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
  81. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
  82. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
  83. package/node_modules/playwright/package.json +1 -1
  84. package/package.json +10 -3
  85. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  86. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  87. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  88. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  89. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
  90. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
@@ -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, StepNumberCondition, UrlPatternCondition } from './skyramp/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) summary['selector'] = rest.selector;
138
- if (rest.url) summary['url'] = rest.url;
139
- if (rest.key) summary['key'] = rest.key;
140
- if (rest.text) summary['text'] = rest.text;
141
- if (rest.timestamp) summary['timestamp'] = rest.timestamp;
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, { stopCondition: _buildStopCondition(data.params), delayBetweenActions: data.params.delayBetweenActions });
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, { stopCondition: _buildStopCondition(data.params) });
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
- filepath,
384
- JSON.stringify(action.snapshotData, null, 2),
385
- 'utf-8'
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: ReplayEngineOptions = {}) {
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: ReplayEngineOptions = {}) {
482
+ private async _runReplay(loadedActions: actions.ActionInContext[], opts: LoadTraceOptions = {}) {
451
483
  if (!loadedActions.length)
452
484
  return;
453
485
 
454
- // Reset current actions and switch to replaying mode so user capture is
455
- // suppressed while the engine drives the browser.
456
- this._actions = [];
457
- this._updateActions();
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
- this._recorder.pageAliases(),
462
- loadedActions,
463
- (action, index) => {
464
- this._actions.push(action);
465
- this._updateActions('reveal');
466
- // Notify the UI of replay progress.
467
- this._page.mainFrame().evaluateExpression(
468
- ((p: { completed: number, total: number }) => {
469
- window.playwrightSetReplayProgress?.(p.completed, p.total);
470
- }).toString(),
471
- { isFunction: true },
472
- { completed: index + 1, total: loadedActions.length },
473
- ).catch(() => { });
474
- },
475
- opts,
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
- ((msg: string) => { window.playwrightSetReplayError?.(msg); }).toString(),
486
- { isFunction: true },
487
- errorMsg,
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
- pageAliases: Map<Page, string>,
99
- actions: actions.ActionInContext[],
100
- onActionCompleted: (action: actions.ActionInContext, index: number) => void,
101
- opts: ReplayEngineOptions = {},
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
- const currentUrl = this._currentUrl();
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
- // Return the URL of the first (main) page.
181
- const firstPage = [...this._pageAliases.keys()].find(p => this._pageAliases.get(p) === 'page');
182
- return firstPage?.mainFrame().url() ?? '';
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/playwright",
3
- "version": "1.58.2-skyramp.8.9.6",
3
+ "version": "1.58.2-skyramp.8.10.0",
4
4
  "description": "Skyramp's fork of Playwright with trace recording for UI test generation",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.7",
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.28",
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.9.6.tgz",
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))
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- const isDocker = require('.');
4
-
5
- process.exitCode = isDocker() ? 0 : 2;
@@ -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
-
@@ -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
- })