@le-space/playwright 0.6.28 → 0.6.29

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 (4) hide show
  1. package/README.md +26 -0
  2. package/index.d.ts +76 -2
  3. package/index.js +143 -50
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -17,3 +17,29 @@ import {
17
17
 
18
18
  `@playwright/test` is a peer dependency. Use a compatible 1.61.x client; remote
19
19
  Playwright servers must use the exact same client/server version.
20
+
21
+ ## Aleph remote Chromium
22
+
23
+ `connectAlephChromium()` is the thin consumer-facing adapter for the dedicated
24
+ Aleph runner. It authenticates the `/version` probe and websocket with the same
25
+ per-run bearer secret, requires HTTPS/WSS, checks the guest is exactly
26
+ Playwright `1.61.1`, and only then calls `chromium.connect()`.
27
+
28
+ ```ts
29
+ const browser = await connectAlephChromium({
30
+ chromium,
31
+ wsEndpoint,
32
+ versionUrl,
33
+ secret,
34
+ })
35
+ ```
36
+
37
+ `buildAlephCostEvidence()` and `formatAlephCostGithubSummary()` keep required
38
+ credit capacity separate from the authoritative before/after account balance
39
+ delta. A balance delta is attributable to one run only when that deployment
40
+ account is not being used concurrently.
41
+
42
+ `selectExpiredAlephPlaywrightRunners()` is the safety boundary used by the
43
+ retention janitor. The repository script requires an explicit repository scope
44
+ and owner key, then erases, owner-signs FORGET for, and verifies each selected
45
+ exact INSTANCE hash. It never cleans by display name alone.
package/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as _playwright_test from '@playwright/test';
2
- import { Page, Locator, BrowserContext } from '@playwright/test';
2
+ import { Browser, Page, Locator, BrowserContext } from '@playwright/test';
3
3
  import { RelayBootstrapPostRecord, RelayBootstrapContent, fetchAlephBootstrapPosts } from '@le-space/aleph-bootstrap';
4
4
  import { eraseInstanceOnCrn, forgetAlephMessages } from '@le-space/core';
5
5
 
6
+ declare const PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
6
7
  declare const SUPPORTED_ALEPH_API_HOSTS: readonly ["https://api2.aleph.im", "https://api.aleph.im"];
7
8
  declare const DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
8
9
  interface RelayWalletAccount {
@@ -28,6 +29,63 @@ interface RelayEvidence {
28
29
  error?: string;
29
30
  [key: string]: unknown;
30
31
  }
32
+ interface AlephChromiumConnector {
33
+ connect(wsEndpoint: string, options?: {
34
+ headers?: Record<string, string>;
35
+ timeout?: number;
36
+ }): Promise<Browser>;
37
+ }
38
+ interface AlephRemoteBrowserOptions {
39
+ chromium: AlephChromiumConnector;
40
+ wsEndpoint: string;
41
+ versionUrl: string;
42
+ secret: string;
43
+ expectedVersion?: string;
44
+ timeoutMs?: number;
45
+ fetch?: typeof fetch;
46
+ }
47
+ interface AlephCreditBalanceSnapshot {
48
+ capturedAt: string;
49
+ apiHost: string;
50
+ creditBalance: number;
51
+ lockedAmount: number;
52
+ }
53
+ interface AlephPricingSnapshot {
54
+ capturedAt: string;
55
+ apiHost: string;
56
+ unitCredit: number;
57
+ computeUnits: number;
58
+ vcpus: number;
59
+ memoryMiB: number;
60
+ diskMiB: number;
61
+ }
62
+ interface AlephCostEvidence {
63
+ paymentType: 'credit';
64
+ startedAt: string;
65
+ finishedAt: string;
66
+ runtimeSeconds: number;
67
+ pricing: AlephPricingSnapshot;
68
+ before: AlephCreditBalanceSnapshot;
69
+ after: AlephCreditBalanceSnapshot;
70
+ requiredCredits: number;
71
+ netAccountCreditDelta: number;
72
+ creditsConsumed: number;
73
+ creditsReturned: number;
74
+ accountingNote: string;
75
+ }
76
+ interface AlephRunnerInstanceCandidate {
77
+ itemHash: string;
78
+ ownerAddress: string;
79
+ instanceName: string;
80
+ createdAt: string;
81
+ status: string;
82
+ }
83
+ interface AlephRunnerJanitorSelection {
84
+ expired: AlephRunnerInstanceCandidate[];
85
+ retained: Array<AlephRunnerInstanceCandidate & {
86
+ reason: string;
87
+ }>;
88
+ }
31
89
  interface RelayAddressPolicy {
32
90
  allowWebTransport?: boolean;
33
91
  allowWebRtcDirect?: boolean;
@@ -116,6 +174,22 @@ interface CreateRelayTestOptions {
116
174
  cleanup?: Omit<CleanupRelayOptions, 'page' | 'account' | 'instanceName' | 'instanceHash'>;
117
175
  }
118
176
  declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
177
+ declare function connectAlephChromium(options: AlephRemoteBrowserOptions): Promise<Browser>;
178
+ declare function buildAlephCostEvidence(options: {
179
+ startedAt: string;
180
+ finishedAt: string;
181
+ pricing: AlephPricingSnapshot;
182
+ before: AlephCreditBalanceSnapshot;
183
+ after: AlephCreditBalanceSnapshot;
184
+ }): AlephCostEvidence;
185
+ declare function formatAlephCostGithubSummary(cost: AlephCostEvidence): string;
186
+ declare function selectExpiredAlephPlaywrightRunners(options: {
187
+ candidates: readonly AlephRunnerInstanceCandidate[];
188
+ ownerAddress: string;
189
+ repository: string;
190
+ now?: number;
191
+ ttlMs?: number;
192
+ }): AlephRunnerJanitorSelection;
119
193
  declare function waitForPubsubSubscriber(page: Page, options: WaitForPubsubSubscriberOptions): Promise<string[]>;
120
194
  declare function installEip1193WalletMock(context: BrowserContext, account: RelayWalletAccount): Promise<void>;
121
195
  declare class RelayButtonDriver {
@@ -181,4 +255,4 @@ declare function createRelayTest(options: CreateRelayTestOptions): _playwright_t
181
255
  relayLifecycle: RelayLifecycleFixture;
182
256
  }, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
183
257
 
184
- export { type CleanupRelayHooks, type CleanupRelayOptions, type CleanupRelayResult, type CreateRelayTestOptions, DEFAULT_ALEPH_SCHEDULER_URL, type ProvisionRelayOptions, type ProvisionedRelay, type RelayAddressPolicy, RelayButtonDriver, type RelayButtonDriverOptions, type RelayEvidence, type RelayEvidenceStep, type RelayLifecycleFixture, type RelayWalletAccount, SUPPORTED_ALEPH_API_HOSTS, type WaitForPubsubSubscriberOptions, appendRelayGithubSummary, cleanupRelay, createRelayEvidence, createRelayTest, findAlephInstanceHash, formatRelayGithubSummary, installEip1193WalletMock, provisionRelay, resolveAlephApiHosts, selectBrowserRelayAddresses, updateRelayEvidenceStep, waitForAlephInstanceDeletion, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
258
+ export { type AlephChromiumConnector, type AlephCostEvidence, type AlephCreditBalanceSnapshot, type AlephPricingSnapshot, type AlephRemoteBrowserOptions, type AlephRunnerInstanceCandidate, type AlephRunnerJanitorSelection, type CleanupRelayHooks, type CleanupRelayOptions, type CleanupRelayResult, type CreateRelayTestOptions, DEFAULT_ALEPH_SCHEDULER_URL, PLAYWRIGHT_RUNNER_VERSION, type ProvisionRelayOptions, type ProvisionedRelay, type RelayAddressPolicy, RelayButtonDriver, type RelayButtonDriverOptions, type RelayEvidence, type RelayEvidenceStep, type RelayLifecycleFixture, type RelayWalletAccount, SUPPORTED_ALEPH_API_HOSTS, type WaitForPubsubSubscriberOptions, appendRelayGithubSummary, buildAlephCostEvidence, cleanupRelay, connectAlephChromium, createRelayEvidence, createRelayTest, findAlephInstanceHash, formatAlephCostGithubSummary, formatRelayGithubSummary, installEip1193WalletMock, provisionRelay, resolveAlephApiHosts, selectBrowserRelayAddresses, selectExpiredAlephPlaywrightRunners, updateRelayEvidenceStep, waitForAlephInstanceDeletion, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
package/index.js CHANGED
@@ -2,17 +2,11 @@
2
2
  import { createHash } from "crypto";
3
3
  import { appendFile, mkdir, writeFile } from "fs/promises";
4
4
  import { dirname } from "path";
5
- import {
6
- fetchAlephBootstrapPosts
7
- } from "@le-space/aleph-bootstrap";
5
+ import { fetchAlephBootstrapPosts } from "@le-space/aleph-bootstrap";
8
6
  import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
9
- import {
10
- test as playwrightTest
11
- } from "@playwright/test";
12
- var SUPPORTED_ALEPH_API_HOSTS = [
13
- "https://api2.aleph.im",
14
- "https://api.aleph.im"
15
- ];
7
+ import { test as playwrightTest } from "@playwright/test";
8
+ var PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
9
+ var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
16
10
  var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
17
11
  var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
18
12
  function normalizeApiOrigin(value) {
@@ -25,12 +19,118 @@ function normalizeApiOrigin(value) {
25
19
  }
26
20
  }
27
21
  function resolveAlephApiHosts(candidates) {
28
- const allowed = new Set(
29
- (candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null)
30
- );
22
+ const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
31
23
  const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
32
24
  return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
33
25
  }
26
+ function requireFiniteNumber(value, label) {
27
+ const number = Number(value);
28
+ if (!Number.isFinite(number)) throw new Error(`${label} must be a finite number`);
29
+ return number;
30
+ }
31
+ async function connectAlephChromium(options) {
32
+ const expectedVersion = options.expectedVersion ?? PLAYWRIGHT_RUNNER_VERSION;
33
+ const secret = options.secret.trim();
34
+ if (!secret) throw new Error("Aleph Playwright secret is required");
35
+ if (!options.wsEndpoint.startsWith("wss://")) {
36
+ throw new Error("Aleph Playwright endpoint must use authenticated WSS");
37
+ }
38
+ if (!options.versionUrl.startsWith("https://")) {
39
+ throw new Error("Aleph Playwright version endpoint must use HTTPS");
40
+ }
41
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
42
+ if (!fetchImpl) throw new Error("A fetch implementation is required for version verification");
43
+ const authorization = `Bearer ${secret}`;
44
+ const response = await fetchImpl(options.versionUrl, {
45
+ headers: { Authorization: authorization },
46
+ cache: "no-store",
47
+ signal: AbortSignal.timeout(options.timeoutMs ?? 3e4)
48
+ });
49
+ if (!response.ok) {
50
+ throw new Error(`Aleph Playwright version endpoint returned HTTP ${response.status}`);
51
+ }
52
+ const payload = await response.json();
53
+ const actualVersion = String(payload.playwrightVersion ?? "");
54
+ if (actualVersion !== expectedVersion) {
55
+ throw new Error(`Playwright client/server version mismatch: client ${expectedVersion}, guest ${actualVersion || "unknown"}`);
56
+ }
57
+ return options.chromium.connect(options.wsEndpoint, {
58
+ headers: { Authorization: authorization },
59
+ timeout: options.timeoutMs ?? 3e4
60
+ });
61
+ }
62
+ function buildAlephCostEvidence(options) {
63
+ const startedAtMs = Date.parse(options.startedAt);
64
+ const finishedAtMs = Date.parse(options.finishedAt);
65
+ if (!Number.isFinite(startedAtMs) || !Number.isFinite(finishedAtMs) || finishedAtMs < startedAtMs) {
66
+ throw new Error("Aleph cost evidence requires ordered ISO start/finish timestamps");
67
+ }
68
+ const unitCredit = requireFiniteNumber(options.pricing.unitCredit, "unitCredit");
69
+ const computeUnits = requireFiniteNumber(options.pricing.computeUnits, "computeUnits");
70
+ const beforeCredits = requireFiniteNumber(options.before.creditBalance, "before.creditBalance");
71
+ const afterCredits = requireFiniteNumber(options.after.creditBalance, "after.creditBalance");
72
+ const netAccountCreditDelta = afterCredits - beforeCredits;
73
+ return {
74
+ paymentType: "credit",
75
+ startedAt: options.startedAt,
76
+ finishedAt: options.finishedAt,
77
+ runtimeSeconds: Math.ceil((finishedAtMs - startedAtMs) / 1e3),
78
+ pricing: options.pricing,
79
+ before: options.before,
80
+ after: options.after,
81
+ requiredCredits: unitCredit * computeUnits,
82
+ netAccountCreditDelta,
83
+ creditsConsumed: Math.max(0, -netAccountCreditDelta),
84
+ creditsReturned: Math.max(0, netAccountCreditDelta),
85
+ accountingNote: "Required credits are Aleph credit-payment capacity, not a time-pro-rated charge. The balance delta is the authoritative account observation for this interval and is attributable to this test only when the deployment account is not used concurrently."
86
+ };
87
+ }
88
+ function formatAlephCostGithubSummary(cost) {
89
+ return `## Aleph remote browser cost
90
+
91
+ | Field | Value |
92
+ | --- | ---: |
93
+ | Payment type | ${cost.paymentType} |
94
+ | Runtime | ${cost.runtimeSeconds} s |
95
+ | Hardware | ${cost.pricing.vcpus} vCPU \xB7 ${cost.pricing.memoryMiB} MiB RAM \xB7 ${cost.pricing.diskMiB} MiB disk |
96
+ | Compute units | ${cost.pricing.computeUnits} |
97
+ | Unit credit requirement | ${cost.pricing.unitCredit} credits |
98
+ | Required credit capacity | ${cost.requiredCredits} credits |
99
+ | Balance before | ${cost.before.creditBalance} credits |
100
+ | Balance after cleanup | ${cost.after.creditBalance} credits |
101
+ | Net account delta | ${cost.netAccountCreditDelta} credits |
102
+ | Credits consumed | ${cost.creditsConsumed} credits |
103
+ | Credits returned | ${cost.creditsReturned} credits |
104
+
105
+ Pricing source: \`${cost.pricing.apiHost}\` at ${cost.pricing.capturedAt}. Balance sources: \`${cost.before.apiHost}\` and \`${cost.after.apiHost}\`.
106
+
107
+ > ${cost.accountingNote}
108
+ `;
109
+ }
110
+ function selectExpiredAlephPlaywrightRunners(options) {
111
+ const owner = options.ownerAddress.trim().toLowerCase();
112
+ const repository = options.repository.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-");
113
+ if (!/^0x[a-f0-9]{40}$/u.test(owner)) throw new Error("Janitor requires an exact EVM owner address");
114
+ if (!repository) throw new Error("Janitor requires a repository name");
115
+ const prefix = `playwright-${repository}-`;
116
+ const now = options.now ?? Date.now();
117
+ const ttlMs = options.ttlMs ?? 60 * 6e4;
118
+ if (!Number.isFinite(ttlMs) || ttlMs < 15 * 6e4) throw new Error("Janitor TTL must be at least 15 minutes");
119
+ const selection = { expired: [], retained: [] };
120
+ for (const candidate of options.candidates) {
121
+ let reason = "";
122
+ const createdAt = Date.parse(candidate.createdAt);
123
+ if (!/^[a-f0-9]{64}$/iu.test(candidate.itemHash)) reason = "invalid exact INSTANCE hash";
124
+ else if (candidate.ownerAddress.toLowerCase() !== owner) reason = "different owner";
125
+ else if (!candidate.instanceName.toLowerCase().startsWith(prefix)) reason = "name outside repository scope";
126
+ else if (!Number.isFinite(createdAt)) reason = "invalid creation timestamp";
127
+ else if (now - createdAt < ttlMs) reason = "within TTL";
128
+ else if (!["processed", "pending"].includes(candidate.status.toLowerCase())) reason = `terminal status ${candidate.status}`;
129
+ if (reason) selection.retained.push({ ...candidate, reason });
130
+ else selection.expired.push(candidate);
131
+ }
132
+ return selection;
133
+ }
34
134
  async function waitForPubsubSubscriber(page, options) {
35
135
  const timeoutMs = options.timeoutMs ?? 3e4;
36
136
  const pollIntervalMs = options.pollIntervalMs ?? 250;
@@ -72,13 +172,13 @@ async function installEip1193WalletMock(context, account) {
72
172
  case "eth_chainId":
73
173
  return "0x1";
74
174
  case "personal_sign": {
75
- const payload = params.find(
76
- (value) => typeof value === "string" && value.startsWith("0x") && value.toLowerCase() !== account.address.toLowerCase()
77
- );
175
+ const payload = params.find((value) => typeof value === "string" && value.startsWith("0x") && value.toLowerCase() !== account.address.toLowerCase());
78
176
  if (typeof payload !== "string") {
79
177
  throw new Error("personal_sign did not contain a payload");
80
178
  }
81
- return account.signMessage({ message: { raw: payload } });
179
+ return account.signMessage({
180
+ message: { raw: payload }
181
+ });
82
182
  }
83
183
  default:
84
184
  throw new Error(`Unsupported E2E wallet method: ${method ?? "missing"}`);
@@ -119,19 +219,26 @@ var RelayButtonDriver = class {
119
219
  };
120
220
  }
121
221
  deployButton() {
122
- return this.page.getByRole("button", { name: this.options.deployButtonName });
222
+ return this.page.getByRole("button", {
223
+ name: this.options.deployButtonName
224
+ });
123
225
  }
124
226
  instance(instanceName) {
125
227
  return this.page.locator("details").filter({ hasText: instanceName }).first();
126
228
  }
127
229
  async prepare(options) {
128
- const launcher = this.page.getByRole("button", { name: this.options.launcherName });
230
+ const launcher = this.page.getByRole("button", {
231
+ name: this.options.launcherName
232
+ });
129
233
  await launcher.waitFor({ state: "visible", timeout: 6e4 });
130
234
  await launcher.click();
131
235
  await this.page.getByPlaceholder(this.options.instanceNamePlaceholder).fill(options.instanceName);
132
236
  await this.page.getByText("Advanced", { exact: true }).click();
133
237
  await this.page.getByPlaceholder(this.options.sshPublicKeyPlaceholder).fill(options.sshPublicKey);
134
- await this.page.getByRole("button", { name: this.options.connectWalletName, exact: true }).click();
238
+ await this.page.getByRole("button", {
239
+ name: this.options.connectWalletName,
240
+ exact: true
241
+ }).click();
135
242
  }
136
243
  async requestDelete(instanceName) {
137
244
  await this.page.getByRole("button", { name: this.options.refreshButtonName }).click().catch(() => {
@@ -154,9 +261,7 @@ async function waitForDeployableManifest(page, options = {}) {
154
261
  const panelText = document.querySelector("aside")?.textContent ?? document.body.textContent ?? "";
155
262
  const failure = states.find((state) => panelText.includes(state));
156
263
  if (failure) return { status: "error", message: failure };
157
- const deployButton = [...document.querySelectorAll("button")].find(
158
- (button) => button.textContent?.trim() === "Deploy Relay"
159
- );
264
+ const deployButton = [...document.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Deploy Relay");
160
265
  return deployButton && !deployButton.disabled ? { status: "ready" } : null;
161
266
  },
162
267
  { states: terminalStates },
@@ -164,18 +269,14 @@ async function waitForDeployableManifest(page, options = {}) {
164
269
  );
165
270
  const result = await outcome.jsonValue();
166
271
  if (result?.status === "error") {
167
- throw new Error(
168
- `Relay Button manifest is not deployable: ${result.message}. Republish the rootfs and update the manifest before provisioning.`
169
- );
272
+ throw new Error(`Relay Button manifest is not deployable: ${result.message}. Republish the rootfs and update the manifest before provisioning.`);
170
273
  }
171
274
  }
172
275
  async function waitForDeploymentUi(page, instanceName, timeoutMs) {
173
276
  const outcome = await page.waitForFunction(
174
277
  (expectedName) => {
175
278
  const instance = [...document.querySelectorAll("details")].find(
176
- (element) => element.textContent?.includes(expectedName) && [...element.querySelectorAll("button")].some(
177
- (button) => button.textContent?.trim() === "Delete"
178
- )
279
+ (element) => element.textContent?.includes(expectedName) && [...element.querySelectorAll("button")].some((button) => button.textContent?.trim() === "Delete")
179
280
  );
180
281
  if (instance?.textContent?.includes("Aleph bootstrap registered")) {
181
282
  return { status: "instance" };
@@ -183,9 +284,7 @@ async function waitForDeploymentUi(page, instanceName, timeoutMs) {
183
284
  const error = document.querySelector("aside.panel .alert.error")?.textContent?.trim();
184
285
  if (error) return { status: "error", message: error };
185
286
  const panelText = document.querySelector("aside")?.textContent ?? "";
186
- const deployButton = [...document.querySelectorAll("button")].find(
187
- (button) => button.textContent?.includes("Deploy")
188
- );
287
+ const deployButton = [...document.querySelectorAll("button")].find((button) => button.textContent?.includes("Deploy"));
189
288
  if (panelText.includes("Deployment failed") && !deployButton?.textContent?.includes("Deploying")) {
190
289
  return { status: "error", message: panelText };
191
290
  }
@@ -310,7 +409,9 @@ async function provisionRelay(page, options) {
310
409
  instanceName: options.instanceName,
311
410
  sshPublicKey: options.sshPublicKey
312
411
  });
313
- await waitForDeployableManifest(page, { timeoutMs: options.manifestTimeoutMs });
412
+ await waitForDeployableManifest(page, {
413
+ timeoutMs: options.manifestTimeoutMs
414
+ });
314
415
  options.onPhase?.("wallet-and-manifest-ready");
315
416
  await driver.deployButton().click();
316
417
  options.onDeploymentSubmitted?.();
@@ -356,20 +457,14 @@ async function waitForAlephInstanceDeletion(options) {
356
457
  if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
357
458
  const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
358
459
  const hosts = resolveAlephApiHosts(options.apiHosts);
359
- const schedulerUrl = new URL(
360
- `/api/v0/allocation/${options.instanceHash}`,
361
- options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL
362
- );
460
+ const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
363
461
  let lastSummary = "Deletion has not been observed yet.";
364
462
  while (Date.now() < deadline) {
365
463
  const observations = [];
366
464
  let replicasForgotten = true;
367
465
  for (const apiHost of hosts) {
368
466
  try {
369
- const response = await fetchImpl(
370
- new URL(`/api/v0/messages/${options.instanceHash}`, apiHost),
371
- { cache: "no-cache" }
372
- );
467
+ const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
373
468
  const payload = await response.json().catch(() => null);
374
469
  const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
375
470
  replicasForgotten &&= forgotten;
@@ -392,9 +487,7 @@ async function waitForAlephInstanceDeletion(options) {
392
487
  if (replicasForgotten && unallocated) return lastSummary;
393
488
  await delay(options.pollIntervalMs ?? 2e3);
394
489
  }
395
- throw new Error(
396
- `Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`
397
- );
490
+ throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
398
491
  }
399
492
  async function cleanupRelay(options) {
400
493
  if (!/^[a-f0-9]{64}$/iu.test(options.instanceHash)) {
@@ -505,12 +598,7 @@ function createRelayEvidence(options) {
505
598
  instanceName: options.instanceName,
506
599
  ownerAddress: options.ownerAddress,
507
600
  startedAt: new Date(options.startedAt ?? Date.now()).toISOString(),
508
- steps: Object.fromEntries(
509
- Object.entries(options.steps).map(([key, label]) => [
510
- key,
511
- { label, status: "pending" }
512
- ])
513
- )
601
+ steps: Object.fromEntries(Object.entries(options.steps).map(([key, label]) => [key, { label, status: "pending" }]))
514
602
  };
515
603
  }
516
604
  function updateRelayEvidenceStep(evidence, step, status, detail = "") {
@@ -638,18 +726,23 @@ function createRelayTest(options) {
638
726
  }
639
727
  export {
640
728
  DEFAULT_ALEPH_SCHEDULER_URL,
729
+ PLAYWRIGHT_RUNNER_VERSION,
641
730
  RelayButtonDriver,
642
731
  SUPPORTED_ALEPH_API_HOSTS,
643
732
  appendRelayGithubSummary,
733
+ buildAlephCostEvidence,
644
734
  cleanupRelay,
735
+ connectAlephChromium,
645
736
  createRelayEvidence,
646
737
  createRelayTest,
647
738
  findAlephInstanceHash,
739
+ formatAlephCostGithubSummary,
648
740
  formatRelayGithubSummary,
649
741
  installEip1193WalletMock,
650
742
  provisionRelay,
651
743
  resolveAlephApiHosts,
652
744
  selectBrowserRelayAddresses,
745
+ selectExpiredAlephPlaywrightRunners,
653
746
  updateRelayEvidenceStep,
654
747
  waitForAlephInstanceDeletion,
655
748
  waitForBootstrapRegistration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/playwright",
3
- "version": "0.6.28",
3
+ "version": "0.6.29",
4
4
  "description": "Reusable Playwright fixtures and Relay Button lifecycle helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,8 +24,8 @@
24
24
  "url": "https://github.com/NiKrause/relay-button/issues"
25
25
  },
26
26
  "dependencies": {
27
- "@le-space/aleph-bootstrap": "0.6.28",
28
- "@le-space/core": "0.6.28"
27
+ "@le-space/aleph-bootstrap": "0.6.29",
28
+ "@le-space/core": "0.6.29"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@playwright/test": ">=1.61.1 <1.62.0"