@le-space/playwright 0.6.28 → 0.6.30
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/README.md +26 -0
- package/index.d.ts +86 -2
- package/index.js +190 -50
- 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;
|
|
@@ -81,6 +139,8 @@ interface CleanupRelayOptions {
|
|
|
81
139
|
account: RelayWalletAccount;
|
|
82
140
|
instanceName: string;
|
|
83
141
|
instanceHash: string;
|
|
142
|
+
/** Guest-published relay-bootstrap POST to verify after graceful VM shutdown. */
|
|
143
|
+
registrationHash?: string;
|
|
84
144
|
driver?: RelayButtonDriver;
|
|
85
145
|
apiHosts?: readonly string[];
|
|
86
146
|
schedulerUrl?: string;
|
|
@@ -99,6 +159,7 @@ interface CleanupRelayResult {
|
|
|
99
159
|
eraseSummary: string;
|
|
100
160
|
forgetSummary: string;
|
|
101
161
|
verificationSummary: string;
|
|
162
|
+
registrationVerificationSummary?: string;
|
|
102
163
|
}
|
|
103
164
|
interface CleanupRelayHooks {
|
|
104
165
|
erase?: typeof eraseInstanceOnCrn;
|
|
@@ -116,6 +177,22 @@ interface CreateRelayTestOptions {
|
|
|
116
177
|
cleanup?: Omit<CleanupRelayOptions, 'page' | 'account' | 'instanceName' | 'instanceHash'>;
|
|
117
178
|
}
|
|
118
179
|
declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
|
|
180
|
+
declare function connectAlephChromium(options: AlephRemoteBrowserOptions): Promise<Browser>;
|
|
181
|
+
declare function buildAlephCostEvidence(options: {
|
|
182
|
+
startedAt: string;
|
|
183
|
+
finishedAt: string;
|
|
184
|
+
pricing: AlephPricingSnapshot;
|
|
185
|
+
before: AlephCreditBalanceSnapshot;
|
|
186
|
+
after: AlephCreditBalanceSnapshot;
|
|
187
|
+
}): AlephCostEvidence;
|
|
188
|
+
declare function formatAlephCostGithubSummary(cost: AlephCostEvidence): string;
|
|
189
|
+
declare function selectExpiredAlephPlaywrightRunners(options: {
|
|
190
|
+
candidates: readonly AlephRunnerInstanceCandidate[];
|
|
191
|
+
ownerAddress: string;
|
|
192
|
+
repository: string;
|
|
193
|
+
now?: number;
|
|
194
|
+
ttlMs?: number;
|
|
195
|
+
}): AlephRunnerJanitorSelection;
|
|
119
196
|
declare function waitForPubsubSubscriber(page: Page, options: WaitForPubsubSubscriberOptions): Promise<string[]>;
|
|
120
197
|
declare function installEip1193WalletMock(context: BrowserContext, account: RelayWalletAccount): Promise<void>;
|
|
121
198
|
declare class RelayButtonDriver {
|
|
@@ -163,6 +240,13 @@ declare function waitForAlephInstanceDeletion(options: {
|
|
|
163
240
|
pollIntervalMs?: number;
|
|
164
241
|
fetch?: typeof fetch;
|
|
165
242
|
}): Promise<string>;
|
|
243
|
+
declare function waitForAlephMessageForgotten(options: {
|
|
244
|
+
messageHash: string;
|
|
245
|
+
apiHosts?: readonly string[];
|
|
246
|
+
timeoutMs?: number;
|
|
247
|
+
pollIntervalMs?: number;
|
|
248
|
+
fetch?: typeof fetch;
|
|
249
|
+
}): Promise<string>;
|
|
166
250
|
declare function cleanupRelay(options: CleanupRelayOptions): Promise<CleanupRelayResult>;
|
|
167
251
|
declare function createRelayEvidence(options: {
|
|
168
252
|
instanceName: string;
|
|
@@ -181,4 +265,4 @@ declare function createRelayTest(options: CreateRelayTestOptions): _playwright_t
|
|
|
181
265
|
relayLifecycle: RelayLifecycleFixture;
|
|
182
266
|
}, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
183
267
|
|
|
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 };
|
|
268
|
+
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
|
@@ -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
|
-
|
|
11
|
-
|
|
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({
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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, {
|
|
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,8 +487,35 @@ async function waitForAlephInstanceDeletion(options) {
|
|
|
392
487
|
if (replicasForgotten && unallocated) return lastSummary;
|
|
393
488
|
await delay(options.pollIntervalMs ?? 2e3);
|
|
394
489
|
}
|
|
490
|
+
throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
|
|
491
|
+
}
|
|
492
|
+
async function waitForAlephMessageForgotten(options) {
|
|
493
|
+
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
494
|
+
if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
|
|
495
|
+
const hosts = resolveAlephApiHosts(options.apiHosts);
|
|
496
|
+
const deadline = Date.now() + (options.timeoutMs ?? 2 * 6e4);
|
|
497
|
+
let lastSummary = "Bootstrap deregistration has not been observed yet.";
|
|
498
|
+
while (Date.now() < deadline) {
|
|
499
|
+
let forgottenOnAllReplicas = true;
|
|
500
|
+
const observations = [];
|
|
501
|
+
for (const apiHost of hosts) {
|
|
502
|
+
try {
|
|
503
|
+
const response = await fetchImpl(new URL(`/api/v0/messages/${options.messageHash}`, apiHost), { cache: "no-cache" });
|
|
504
|
+
const payload = await response.json().catch(() => null);
|
|
505
|
+
const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
|
|
506
|
+
forgottenOnAllReplicas &&= forgotten;
|
|
507
|
+
observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
forgottenOnAllReplicas = false;
|
|
510
|
+
observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
lastSummary = observations.join("; ");
|
|
514
|
+
if (forgottenOnAllReplicas) return lastSummary;
|
|
515
|
+
await delay(options.pollIntervalMs ?? 2e3);
|
|
516
|
+
}
|
|
395
517
|
throw new Error(
|
|
396
|
-
`Aleph
|
|
518
|
+
`Aleph bootstrap registration ${options.messageHash} was not deregistered within ${options.timeoutMs ?? 2 * 6e4}ms: ${lastSummary}`
|
|
397
519
|
);
|
|
398
520
|
}
|
|
399
521
|
async function cleanupRelay(options) {
|
|
@@ -423,13 +545,21 @@ async function cleanupRelay(options) {
|
|
|
423
545
|
pollIntervalMs: options.pollIntervalMs,
|
|
424
546
|
fetch: fetchImpl
|
|
425
547
|
});
|
|
548
|
+
const registrationVerificationSummary2 = options.registrationHash ? await waitForAlephMessageForgotten({
|
|
549
|
+
messageHash: options.registrationHash,
|
|
550
|
+
apiHosts: hosts,
|
|
551
|
+
timeoutMs: options.timeoutMs,
|
|
552
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
553
|
+
fetch: fetchImpl
|
|
554
|
+
}) : void 0;
|
|
426
555
|
return {
|
|
427
556
|
instanceHash: options.instanceHash,
|
|
428
557
|
uiDeleteRequested,
|
|
429
558
|
fallbackUsed: false,
|
|
430
559
|
eraseSummary: "Relay Button UI requested runtime erase",
|
|
431
560
|
forgetSummary: "Relay Button UI submitted FORGET",
|
|
432
|
-
verificationSummary: verificationSummary2
|
|
561
|
+
verificationSummary: verificationSummary2,
|
|
562
|
+
registrationVerificationSummary: registrationVerificationSummary2
|
|
433
563
|
};
|
|
434
564
|
} catch {
|
|
435
565
|
}
|
|
@@ -491,13 +621,21 @@ async function cleanupRelay(options) {
|
|
|
491
621
|
pollIntervalMs: options.pollIntervalMs,
|
|
492
622
|
fetch: fetchImpl
|
|
493
623
|
});
|
|
624
|
+
const registrationVerificationSummary = options.registrationHash ? await waitForAlephMessageForgotten({
|
|
625
|
+
messageHash: options.registrationHash,
|
|
626
|
+
apiHosts: hosts,
|
|
627
|
+
timeoutMs: options.timeoutMs,
|
|
628
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
629
|
+
fetch: fetchImpl
|
|
630
|
+
}) : void 0;
|
|
494
631
|
return {
|
|
495
632
|
instanceHash: options.instanceHash,
|
|
496
633
|
uiDeleteRequested,
|
|
497
634
|
fallbackUsed: true,
|
|
498
635
|
eraseSummary,
|
|
499
636
|
forgetSummary,
|
|
500
|
-
verificationSummary
|
|
637
|
+
verificationSummary,
|
|
638
|
+
registrationVerificationSummary
|
|
501
639
|
};
|
|
502
640
|
}
|
|
503
641
|
function createRelayEvidence(options) {
|
|
@@ -505,12 +643,7 @@ function createRelayEvidence(options) {
|
|
|
505
643
|
instanceName: options.instanceName,
|
|
506
644
|
ownerAddress: options.ownerAddress,
|
|
507
645
|
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
|
-
)
|
|
646
|
+
steps: Object.fromEntries(Object.entries(options.steps).map(([key, label]) => [key, { label, status: "pending" }]))
|
|
514
647
|
};
|
|
515
648
|
}
|
|
516
649
|
function updateRelayEvidenceStep(evidence, step, status, detail = "") {
|
|
@@ -617,6 +750,7 @@ function createRelayTest(options) {
|
|
|
617
750
|
account: options.account,
|
|
618
751
|
instanceName: relay.instanceName,
|
|
619
752
|
instanceHash: relay.instanceHash,
|
|
753
|
+
registrationHash: relay.registration.itemHash ?? relay.registration.hash ?? void 0,
|
|
620
754
|
driver: relay.driver
|
|
621
755
|
})
|
|
622
756
|
);
|
|
@@ -638,20 +772,26 @@ function createRelayTest(options) {
|
|
|
638
772
|
}
|
|
639
773
|
export {
|
|
640
774
|
DEFAULT_ALEPH_SCHEDULER_URL,
|
|
775
|
+
PLAYWRIGHT_RUNNER_VERSION,
|
|
641
776
|
RelayButtonDriver,
|
|
642
777
|
SUPPORTED_ALEPH_API_HOSTS,
|
|
643
778
|
appendRelayGithubSummary,
|
|
779
|
+
buildAlephCostEvidence,
|
|
644
780
|
cleanupRelay,
|
|
781
|
+
connectAlephChromium,
|
|
645
782
|
createRelayEvidence,
|
|
646
783
|
createRelayTest,
|
|
647
784
|
findAlephInstanceHash,
|
|
785
|
+
formatAlephCostGithubSummary,
|
|
648
786
|
formatRelayGithubSummary,
|
|
649
787
|
installEip1193WalletMock,
|
|
650
788
|
provisionRelay,
|
|
651
789
|
resolveAlephApiHosts,
|
|
652
790
|
selectBrowserRelayAddresses,
|
|
791
|
+
selectExpiredAlephPlaywrightRunners,
|
|
653
792
|
updateRelayEvidenceStep,
|
|
654
793
|
waitForAlephInstanceDeletion,
|
|
794
|
+
waitForAlephMessageForgotten,
|
|
655
795
|
waitForBootstrapRegistration,
|
|
656
796
|
waitForDeployableManifest,
|
|
657
797
|
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.30",
|
|
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
|
-
"@le-space/core": "0.6.
|
|
27
|
+
"@le-space/aleph-bootstrap": "0.6.30",
|
|
28
|
+
"@le-space/core": "0.6.30"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@playwright/test": ">=1.61.1 <1.62.0"
|