@le-space/playwright 0.6.29 → 0.6.31

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.
@@ -0,0 +1,66 @@
1
+ // src/aleph-instance.ts
2
+ var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
3
+ var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
4
+ var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ function normalizeApiOrigin(value) {
9
+ try {
10
+ const url = new URL(value);
11
+ if (API3_HOST_PATTERN.test(url.hostname)) return null;
12
+ return url.origin;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function resolveAlephApiHosts(candidates) {
18
+ const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
19
+ const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
20
+ return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
21
+ }
22
+ async function waitForAlephInstanceDeletion(options) {
23
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
24
+ if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
25
+ const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
26
+ const hosts = resolveAlephApiHosts(options.apiHosts);
27
+ const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
28
+ let lastSummary = "Deletion has not been observed yet.";
29
+ while (Date.now() < deadline) {
30
+ const observations = [];
31
+ let replicasForgotten = true;
32
+ for (const apiHost of hosts) {
33
+ try {
34
+ const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
35
+ const payload = await response.json().catch(() => null);
36
+ const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
37
+ replicasForgotten &&= forgotten;
38
+ observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
39
+ } catch (error) {
40
+ replicasForgotten = false;
41
+ observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ let unallocated = false;
45
+ try {
46
+ const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
47
+ const payload = await response.json().catch(() => null);
48
+ unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
49
+ observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
50
+ } catch (error) {
51
+ observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
52
+ }
53
+ lastSummary = observations.join("; ");
54
+ if (replicasForgotten && unallocated) return lastSummary;
55
+ await delay(options.pollIntervalMs ?? 2e3);
56
+ }
57
+ throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
58
+ }
59
+
60
+ export {
61
+ SUPPORTED_ALEPH_API_HOSTS,
62
+ DEFAULT_ALEPH_SCHEDULER_URL,
63
+ delay,
64
+ resolveAlephApiHosts,
65
+ waitForAlephInstanceDeletion
66
+ };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ declare function parseCleanupCliArgs(argv: readonly string[], env: NodeJS.ProcessEnv): {
3
+ instanceHash: string;
4
+ apiHosts: string[];
5
+ evidencePath: string;
6
+ reason: string;
7
+ };
8
+ declare function runCleanupCli(argv?: readonly string[], env?: NodeJS.ProcessEnv): Promise<void>;
9
+
10
+ export { parseCleanupCliArgs, runCleanupCli };
package/cleanup-cli.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveAlephApiHosts,
4
+ waitForAlephInstanceDeletion
5
+ } from "./chunk-YAODEYKV.js";
6
+
7
+ // src/cleanup-cli.ts
8
+ import { createHash } from "crypto";
9
+ import { appendFile, mkdir, writeFile } from "fs/promises";
10
+ import { dirname } from "path";
11
+ import process from "process";
12
+ import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
13
+ function parseCleanupCliArgs(argv, env) {
14
+ const flags = /* @__PURE__ */ new Map();
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const argument = argv[index];
17
+ if (!argument.startsWith("--")) continue;
18
+ const equals = argument.indexOf("=");
19
+ if (equals > -1) {
20
+ flags.set(argument.slice(2, equals), argument.slice(equals + 1));
21
+ } else {
22
+ const value = argv[index + 1];
23
+ if (value == null || value.startsWith("--")) throw new Error(`Missing value for --${argument.slice(2)}`);
24
+ flags.set(argument.slice(2), value);
25
+ index += 1;
26
+ }
27
+ }
28
+ const instanceHash = (flags.get("instance-hash") ?? env.ALEPH_PLAYWRIGHT_INSTANCE_HASH ?? "").trim();
29
+ if (!/^[a-f0-9]{64}$/iu.test(instanceHash)) {
30
+ throw new Error("Cleanup requires one exact INSTANCE hash (--instance-hash or ALEPH_PLAYWRIGHT_INSTANCE_HASH)");
31
+ }
32
+ const hostCandidates = (flags.get("api-hosts") ?? env.ALEPH_VM_API_HOSTS ?? "").split(/[\s,]+/u).filter(Boolean);
33
+ return {
34
+ instanceHash,
35
+ apiHosts: resolveAlephApiHosts(hostCandidates.length > 0 ? hostCandidates : void 0),
36
+ evidencePath: (flags.get("evidence-path") ?? env.EVIDENCE_PATH ?? "").trim() || `playwright-runner-cleanup-${instanceHash.slice(0, 12)}.json`,
37
+ reason: (flags.get("reason") ?? "").trim() || `Ephemeral Playwright runner cleanup for ${instanceHash}`
38
+ };
39
+ }
40
+ function importOptional(specifier) {
41
+ return import(specifier);
42
+ }
43
+ async function createSigner(privateKey) {
44
+ const normalizedKey = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
45
+ try {
46
+ const { Wallet } = await importOptional("ethers");
47
+ const wallet = new Wallet(normalizedKey);
48
+ return { address: await wallet.getAddress(), sign: (payload) => wallet.signMessage(payload) };
49
+ } catch (error) {
50
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
51
+ }
52
+ try {
53
+ const { privateKeyToAccount } = await importOptional("viem/accounts");
54
+ const account = privateKeyToAccount(normalizedKey);
55
+ return { address: account.address, sign: (payload) => account.signMessage({ message: payload }) };
56
+ } catch (error) {
57
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
58
+ }
59
+ throw new Error('playwright-runner-cleanup needs either "ethers" or "viem" to sign the FORGET message');
60
+ }
61
+ async function runCleanupCli(argv = process.argv.slice(2), env = process.env) {
62
+ const options = parseCleanupCliArgs(argv, env);
63
+ const privateKey = env.ALEPH_PRIVATE_KEY?.trim();
64
+ if (!privateKey) throw new Error("ALEPH_PRIVATE_KEY is required");
65
+ const identity = await createSigner(privateKey);
66
+ const signer = (_sender, payload) => identity.sign(payload);
67
+ const fetchImpl = globalThis.fetch.bind(globalThis);
68
+ let erase;
69
+ for (const apiHost of options.apiHosts) {
70
+ try {
71
+ erase = await eraseInstanceOnCrn({
72
+ sender: identity.address,
73
+ signer,
74
+ instanceHash: options.instanceHash,
75
+ fetch: fetchImpl,
76
+ apiHost
77
+ });
78
+ break;
79
+ } catch {
80
+ }
81
+ }
82
+ let forget = null;
83
+ for (const apiHost of options.apiHosts) {
84
+ try {
85
+ forget = await forgetAlephMessages({
86
+ sender: identity.address,
87
+ hashes: [options.instanceHash],
88
+ reason: options.reason,
89
+ signer,
90
+ hasher: (content) => createHash("sha256").update(content).digest("hex"),
91
+ fetch: fetchImpl,
92
+ apiHost,
93
+ sync: true
94
+ });
95
+ if (forget.status === "rejected") throw new Error("FORGET rejected");
96
+ break;
97
+ } catch {
98
+ forget = null;
99
+ }
100
+ }
101
+ if (!forget) throw new Error(`Owner-signed FORGET failed for ${options.instanceHash}`);
102
+ const verification = await waitForAlephInstanceDeletion({
103
+ instanceHash: options.instanceHash,
104
+ apiHosts: options.apiHosts,
105
+ fetch: fetchImpl
106
+ });
107
+ await mkdir(dirname(options.evidencePath) || ".", { recursive: true });
108
+ await writeFile(
109
+ options.evidencePath,
110
+ `${JSON.stringify(
111
+ {
112
+ instanceHash: options.instanceHash,
113
+ owner: identity.address,
114
+ apiHosts: options.apiHosts,
115
+ erase,
116
+ forget: forget.itemHash,
117
+ verification
118
+ },
119
+ null,
120
+ 2
121
+ )}
122
+ `
123
+ );
124
+ if (env.GITHUB_OUTPUT) await appendFile(env.GITHUB_OUTPUT, `evidence_path=${options.evidencePath}
125
+ `);
126
+ if (env.GITHUB_STEP_SUMMARY) {
127
+ await appendFile(
128
+ env.GITHUB_STEP_SUMMARY,
129
+ `
130
+ ## Aleph Playwright cleanup
131
+
132
+ - Exact INSTANCE: \`${options.instanceHash}\`
133
+ - Runtime erase: \`${erase?.status ?? "unavailable"}\`
134
+ - Owner FORGET: \`${forget.status}\`
135
+ - Verification: ${verification}
136
+ `
137
+ );
138
+ }
139
+ }
140
+ var invokedDirectly = process.argv[1]?.endsWith("cleanup-cli.ts") || process.argv[1]?.endsWith("cleanup-cli.js") || process.argv[1]?.endsWith("playwright-runner-cleanup");
141
+ if (invokedDirectly) {
142
+ runCleanupCli().catch((error) => {
143
+ console.error(error instanceof Error ? error.message : String(error));
144
+ process.exitCode = 1;
145
+ });
146
+ }
147
+ export {
148
+ parseCleanupCliArgs,
149
+ runCleanupCli
150
+ };
package/index.d.ts CHANGED
@@ -3,9 +3,19 @@ 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";
7
6
  declare const SUPPORTED_ALEPH_API_HOSTS: readonly ["https://api2.aleph.im", "https://api.aleph.im"];
8
7
  declare const DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
8
+ declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
9
+ declare function waitForAlephInstanceDeletion(options: {
10
+ instanceHash: string;
11
+ apiHosts?: readonly string[];
12
+ schedulerUrl?: string;
13
+ timeoutMs?: number;
14
+ pollIntervalMs?: number;
15
+ fetch?: typeof fetch;
16
+ }): Promise<string>;
17
+
18
+ declare const PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
9
19
  interface RelayWalletAccount {
10
20
  address: string;
11
21
  signMessage(args: {
@@ -100,7 +110,7 @@ interface WaitForPubsubSubscriberOptions {
100
110
  stableForMs?: number;
101
111
  }
102
112
  interface RelayButtonDriverOptions {
103
- launcherName?: RegExp;
113
+ launcherName?: string | RegExp;
104
114
  instanceNamePlaceholder?: string;
105
115
  sshPublicKeyPlaceholder?: string;
106
116
  connectWalletName?: string;
@@ -139,6 +149,8 @@ interface CleanupRelayOptions {
139
149
  account: RelayWalletAccount;
140
150
  instanceName: string;
141
151
  instanceHash: string;
152
+ /** Guest-published relay-bootstrap POST to verify after graceful VM shutdown. */
153
+ registrationHash?: string;
142
154
  driver?: RelayButtonDriver;
143
155
  apiHosts?: readonly string[];
144
156
  schedulerUrl?: string;
@@ -157,6 +169,7 @@ interface CleanupRelayResult {
157
169
  eraseSummary: string;
158
170
  forgetSummary: string;
159
171
  verificationSummary: string;
172
+ registrationVerificationSummary?: string;
160
173
  }
161
174
  interface CleanupRelayHooks {
162
175
  erase?: typeof eraseInstanceOnCrn;
@@ -173,7 +186,6 @@ interface CreateRelayTestOptions {
173
186
  evidence: RelayEvidence;
174
187
  cleanup?: Omit<CleanupRelayOptions, 'page' | 'account' | 'instanceName' | 'instanceHash'>;
175
188
  }
176
- declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
177
189
  declare function connectAlephChromium(options: AlephRemoteBrowserOptions): Promise<Browser>;
178
190
  declare function buildAlephCostEvidence(options: {
179
191
  startedAt: string;
@@ -229,10 +241,9 @@ declare function waitForBootstrapRegistration(options: {
229
241
  }): Promise<RelayBootstrapPostRecord>;
230
242
  declare function selectBrowserRelayAddresses(content: Pick<RelayBootstrapContent, 'browserMultiaddrs' | 'multiaddrs'>, policy?: RelayAddressPolicy): string[];
231
243
  declare function provisionRelay(page: Page, options: ProvisionRelayOptions): Promise<ProvisionedRelay>;
232
- declare function waitForAlephInstanceDeletion(options: {
233
- instanceHash: string;
244
+ declare function waitForAlephMessageForgotten(options: {
245
+ messageHash: string;
234
246
  apiHosts?: readonly string[];
235
- schedulerUrl?: string;
236
247
  timeoutMs?: number;
237
248
  pollIntervalMs?: number;
238
249
  fetch?: typeof fetch;
@@ -255,4 +266,4 @@ declare function createRelayTest(options: CreateRelayTestOptions): _playwright_t
255
266
  relayLifecycle: RelayLifecycleFixture;
256
267
  }, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
257
268
 
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 };
269
+ 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, waitForAlephMessageForgotten, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
package/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ DEFAULT_ALEPH_SCHEDULER_URL,
3
+ SUPPORTED_ALEPH_API_HOSTS,
4
+ delay,
5
+ resolveAlephApiHosts,
6
+ waitForAlephInstanceDeletion
7
+ } from "./chunk-YAODEYKV.js";
8
+
1
9
  // src/index.ts
2
10
  import { createHash } from "crypto";
3
11
  import { appendFile, mkdir, writeFile } from "fs/promises";
@@ -6,23 +14,6 @@ import { fetchAlephBootstrapPosts } from "@le-space/aleph-bootstrap";
6
14
  import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
7
15
  import { test as playwrightTest } from "@playwright/test";
8
16
  var PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
9
- var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
10
- var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
11
- var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
12
- function normalizeApiOrigin(value) {
13
- try {
14
- const url = new URL(value);
15
- if (API3_HOST_PATTERN.test(url.hostname)) return null;
16
- return url.origin;
17
- } catch {
18
- return null;
19
- }
20
- }
21
- function resolveAlephApiHosts(candidates) {
22
- const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
23
- const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
24
- return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
25
- }
26
17
  function requireFiniteNumber(value, label) {
27
18
  const number = Number(value);
28
19
  if (!Number.isFinite(number)) throw new Error(`${label} must be a finite number`);
@@ -209,7 +200,7 @@ var RelayButtonDriver = class {
209
200
  constructor(page, options = {}) {
210
201
  this.page = page;
211
202
  this.options = {
212
- launcherName: options.launcherName ?? /(?:Sponsor Relay|Relay Button|Relay)/,
203
+ launcherName: options.launcherName ?? "Relay Button",
213
204
  instanceNamePlaceholder: options.instanceNamePlaceholder ?? "Instance name",
214
205
  sshPublicKeyPlaceholder: options.sshPublicKeyPlaceholder ?? "SSH public key",
215
206
  connectWalletName: options.connectWalletName ?? "Connect MetaMask",
@@ -298,9 +289,6 @@ async function waitForDeploymentUi(page, instanceName, timeoutMs) {
298
289
  throw new Error(`Relay Button deployment failed: ${result.message}`);
299
290
  }
300
291
  }
301
- function delay(ms) {
302
- return new Promise((resolve) => setTimeout(resolve, ms));
303
- }
304
292
  async function findAlephInstanceHash(options) {
305
293
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
306
294
  if (!fetchImpl) throw new Error("A fetch implementation is required for INSTANCE lookup");
@@ -452,42 +440,34 @@ async function provisionRelay(page, options) {
452
440
  driver
453
441
  };
454
442
  }
455
- async function waitForAlephInstanceDeletion(options) {
443
+ async function waitForAlephMessageForgotten(options) {
456
444
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
457
445
  if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
458
- const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
459
446
  const hosts = resolveAlephApiHosts(options.apiHosts);
460
- const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
461
- let lastSummary = "Deletion has not been observed yet.";
447
+ const deadline = Date.now() + (options.timeoutMs ?? 2 * 6e4);
448
+ let lastSummary = "Bootstrap deregistration has not been observed yet.";
462
449
  while (Date.now() < deadline) {
450
+ let forgottenOnAllReplicas = true;
463
451
  const observations = [];
464
- let replicasForgotten = true;
465
452
  for (const apiHost of hosts) {
466
453
  try {
467
- const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
454
+ const response = await fetchImpl(new URL(`/api/v0/messages/${options.messageHash}`, apiHost), { cache: "no-cache" });
468
455
  const payload = await response.json().catch(() => null);
469
456
  const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
470
- replicasForgotten &&= forgotten;
457
+ forgottenOnAllReplicas &&= forgotten;
471
458
  observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
472
459
  } catch (error) {
473
- replicasForgotten = false;
460
+ forgottenOnAllReplicas = false;
474
461
  observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
475
462
  }
476
463
  }
477
- let unallocated = false;
478
- try {
479
- const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
480
- const payload = await response.json().catch(() => null);
481
- unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
482
- observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
483
- } catch (error) {
484
- observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
485
- }
486
464
  lastSummary = observations.join("; ");
487
- if (replicasForgotten && unallocated) return lastSummary;
465
+ if (forgottenOnAllReplicas) return lastSummary;
488
466
  await delay(options.pollIntervalMs ?? 2e3);
489
467
  }
490
- throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
468
+ throw new Error(
469
+ `Aleph bootstrap registration ${options.messageHash} was not deregistered within ${options.timeoutMs ?? 2 * 6e4}ms: ${lastSummary}`
470
+ );
491
471
  }
492
472
  async function cleanupRelay(options) {
493
473
  if (!/^[a-f0-9]{64}$/iu.test(options.instanceHash)) {
@@ -516,13 +496,21 @@ async function cleanupRelay(options) {
516
496
  pollIntervalMs: options.pollIntervalMs,
517
497
  fetch: fetchImpl
518
498
  });
499
+ const registrationVerificationSummary2 = options.registrationHash ? await waitForAlephMessageForgotten({
500
+ messageHash: options.registrationHash,
501
+ apiHosts: hosts,
502
+ timeoutMs: options.timeoutMs,
503
+ pollIntervalMs: options.pollIntervalMs,
504
+ fetch: fetchImpl
505
+ }) : void 0;
519
506
  return {
520
507
  instanceHash: options.instanceHash,
521
508
  uiDeleteRequested,
522
509
  fallbackUsed: false,
523
510
  eraseSummary: "Relay Button UI requested runtime erase",
524
511
  forgetSummary: "Relay Button UI submitted FORGET",
525
- verificationSummary: verificationSummary2
512
+ verificationSummary: verificationSummary2,
513
+ registrationVerificationSummary: registrationVerificationSummary2
526
514
  };
527
515
  } catch {
528
516
  }
@@ -584,13 +572,21 @@ async function cleanupRelay(options) {
584
572
  pollIntervalMs: options.pollIntervalMs,
585
573
  fetch: fetchImpl
586
574
  });
575
+ const registrationVerificationSummary = options.registrationHash ? await waitForAlephMessageForgotten({
576
+ messageHash: options.registrationHash,
577
+ apiHosts: hosts,
578
+ timeoutMs: options.timeoutMs,
579
+ pollIntervalMs: options.pollIntervalMs,
580
+ fetch: fetchImpl
581
+ }) : void 0;
587
582
  return {
588
583
  instanceHash: options.instanceHash,
589
584
  uiDeleteRequested,
590
585
  fallbackUsed: true,
591
586
  eraseSummary,
592
587
  forgetSummary,
593
- verificationSummary
588
+ verificationSummary,
589
+ registrationVerificationSummary
594
590
  };
595
591
  }
596
592
  function createRelayEvidence(options) {
@@ -705,6 +701,7 @@ function createRelayTest(options) {
705
701
  account: options.account,
706
702
  instanceName: relay.instanceName,
707
703
  instanceHash: relay.instanceHash,
704
+ registrationHash: relay.registration.itemHash ?? relay.registration.hash ?? void 0,
708
705
  driver: relay.driver
709
706
  })
710
707
  );
@@ -745,6 +742,7 @@ export {
745
742
  selectExpiredAlephPlaywrightRunners,
746
743
  updateRelayEvidenceStep,
747
744
  waitForAlephInstanceDeletion,
745
+ waitForAlephMessageForgotten,
748
746
  waitForBootstrapRegistration,
749
747
  waitForDeployableManifest,
750
748
  waitForPubsubSubscriber,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/playwright",
3
- "version": "0.6.29",
3
+ "version": "0.6.31",
4
4
  "description": "Reusable Playwright fixtures and Relay Button lifecycle helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,9 @@
12
12
  "import": "./index.js"
13
13
  }
14
14
  },
15
+ "bin": {
16
+ "playwright-runner-cleanup": "./cleanup-cli.js"
17
+ },
15
18
  "publishConfig": {
16
19
  "access": "public"
17
20
  },
@@ -24,8 +27,8 @@
24
27
  "url": "https://github.com/NiKrause/relay-button/issues"
25
28
  },
26
29
  "dependencies": {
27
- "@le-space/aleph-bootstrap": "0.6.29",
28
- "@le-space/core": "0.6.29"
30
+ "@le-space/aleph-bootstrap": "0.6.31",
31
+ "@le-space/core": "0.6.31"
29
32
  },
30
33
  "peerDependencies": {
31
34
  "@playwright/test": ">=1.61.1 <1.62.0"