@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.
- package/chunk-YAODEYKV.js +66 -0
- package/cleanup-cli.d.ts +10 -0
- package/cleanup-cli.js +150 -0
- package/index.d.ts +18 -7
- package/index.js +40 -42
- package/package.json +6 -3
|
@@ -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
|
+
};
|
package/cleanup-cli.d.ts
ADDED
|
@@ -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
|
|
233
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
461
|
-
let lastSummary = "
|
|
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.
|
|
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
|
-
|
|
457
|
+
forgottenOnAllReplicas &&= forgotten;
|
|
471
458
|
observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
|
|
472
459
|
} catch (error) {
|
|
473
|
-
|
|
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 (
|
|
465
|
+
if (forgottenOnAllReplicas) return lastSummary;
|
|
488
466
|
await delay(options.pollIntervalMs ?? 2e3);
|
|
489
467
|
}
|
|
490
|
-
throw new Error(
|
|
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.
|
|
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.
|
|
28
|
-
"@le-space/core": "0.6.
|
|
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"
|