Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .github/workflows/pr-playwright-browser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,6 @@ jobs:
exit 1
fi

- name: Pull base images (with retry)
shell: bash
run: |
for i in 1 2 3; do
docker pull node:20-alpine && break
echo "Pull attempt $i failed, retrying in 30s..."
sleep 30
done

- name: Build CI containers
shell: bash
run: docker compose -f docker-compose.playwright.yml build app bootstrap-runner playwright-runner
Expand Down
3 changes: 0 additions & 3 deletions Dockerfile.playwright
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ COPY package.json package-lock.json* ./
COPY prisma ./prisma
RUN npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000

# Install Chromium and its system dependencies.
RUN npx playwright install chromium --with-deps

# Copy e2e suite and the CI framework types it imports.
COPY e2e/ ./e2e/
COPY scripts/ci/framework/ ./scripts/ci/framework/
Expand Down
1 change: 0 additions & 1 deletion e2e/tests/create-wallet-ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ async function createWalletThroughUi(
const clearStakeKey = mode === "legacy";

await page.goto("/wallets/new-wallet-flow/save");
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});

await page.getByLabel("Name", { exact: true }).fill(walletName);
await page.getByLabel(/Description/i).fill(`Playwright ${mode} wallet creation coverage`);
Expand Down
1 change: 0 additions & 1 deletion e2e/tests/rejected-signing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ test.describe("rejected wallet signing", () => {
).toEqual([]);

await page.goto(`/wallets/${wallet.walletId}/transactions`);
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});
await expect(page.locator('[data-testid^="tx-card-"]')).toHaveCount(0);
});

Expand Down
1 change: 0 additions & 1 deletion e2e/tests/responsive-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ for (const viewport of VIEWPORTS) {
await expect(
page.getByRole("link", { name: "New Wallet" }),
).toBeVisible({ timeout: 60_000 });
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});
await expectNoHorizontalOverflow(page, "wallet list");
});

Expand Down
3 changes: 0 additions & 3 deletions e2e/tests/ring-transfer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ async function waitForUtxoSelectorLoaded(
for (let attempt = 1; attempt <= 3; attempt += 1) {
diagnostics.push(`loading new transaction page attempt ${attempt}`);
await page.goto(newTransactionUrl);
await page.waitForLoadState("networkidle", { timeout: 60_000 }).catch(() => {});

const loaded = await page
.waitForSelector('[data-testid="utxo-selector"][data-loaded="true"]', {
Expand Down Expand Up @@ -414,7 +413,6 @@ test.describe("ring transfer", () => {
const transactionsUrl = `/wallets/${srcWallet.walletId}/transactions`;
await page.goto(transactionsUrl);
await expect(page).toHaveURL(new RegExp(`/wallets/${srcWallet.walletId}/transactions$`));
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});

// Extract the newly created transaction's ID from the first pending tx card
const txCard = page.locator('[data-testid^="tx-card-"]').first();
Expand Down Expand Up @@ -448,7 +446,6 @@ test.describe("ring transfer", () => {
await authenticateAs(page, 1);

await page.goto(`/wallets/${srcWallet.walletId}/transactions`);
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});

// Confirm the tx card is still pending (proposer signed, threshold not yet met)
await page.waitForSelector(`[data-testid="tx-card-${transactionId}"]`, {
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Runs after the early discovery and ADA route-health checks, before request-heavy

Runs when both `legacy` and `sdk` wallets are in context. Requires `CI_DREP_ANCHOR_URL`.

For each wallet type the scenario runs a pre-hygiene step followed by two sequential phases — register then retire — leaving the wallet in its pre-test DRep state:
For each wallet type the scenario runs a pre-hygiene step followed by two sequential phases — register then retire — leaving the wallet in its pre-test DRep state. The legacy and SDK wallet branches run in parallel because they spend from distinct multisig wallets, while each branch keeps its register-before-retire ordering:

**Pre-hygiene step** — checks on-chain DRep state via `GET /api/v1/drepInfo`. If the DRep is already registered (e.g. from a previous incomplete run), it proposes a `retire` tx, signs with both signers, and waits for on-chain confirmation. If the broadcast is rejected with `DRepNotRegistered` or similar errors, the credential is treated as already clean (stale Blockfrost cache false-positive) and the step succeeds silently.

Expand All @@ -208,7 +208,7 @@ For each wallet type the scenario runs a pre-hygiene step followed by two sequen
3. Signer 1 (`CI_MNEMONIC_2`, index 1) adds a payment-key witness, no broadcast.
4. Signer 2 (`CI_MNEMONIC_3`, index 2) adds a payment-key witness and broadcasts.
5. Assert the transaction is cleared from pending.
6. Poll `freeUtxos?fresh=true` until the spent inputs are no longer unspent on-chain (confirms block inclusion before the next phase). Up to 30 retries × 8 s = 4 minutes.
6. Poll `freeUtxos?fresh=true` until the spent inputs are no longer unspent on-chain (confirms block inclusion before the next phase). Up to 48 retries × 5 s = 4 minutes.
7. Repeat steps 1–6 with `action: "retire"`.

**Why payment-key witnesses are sufficient for DRep cert:**
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci/scenarios/flows/utxoShapeFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ async function pollUntilUtxosConsumed(args: {
maxRetries?: number;
retryDelayMs?: number;
}): Promise<{ attempts: number }> {
const maxRetries = args.maxRetries ?? 30;
const retryDelayMs = args.retryDelayMs ?? 8000;
const maxRetries = args.maxRetries ?? 48;
const retryDelayMs = args.retryDelayMs ?? 5000;
const spent = new Set(args.spentUtxoRefs.map(key));
for (let attempt = 0; attempt < maxRetries; attempt++) {
if (attempt > 0) {
Expand Down
109 changes: 58 additions & 51 deletions scripts/ci/scenarios/steps/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function fetchUtxoRefs(args: {
* the result. This confirms the cert tx has been included in a block and its
* inputs are no longer unspent on-chain.
*
* Preprod block time is ~20 s. We retry every 8 s for up to 4 minutes.
* Preprod block time is ~20 s. We retry every 5 s for up to 4 minutes.
*/
async function pollUntilUtxosConsumed(args: {
ctx: CIBootstrapContext;
Expand All @@ -54,8 +54,8 @@ async function pollUntilUtxosConsumed(args: {
retryDelayMs?: number;
}): Promise<{ attempts: number }> {
const { ctx, walletId, token, botAddress, spentUtxoRefs } = args;
const maxRetries = args.maxRetries ?? 30;
const retryDelayMs = args.retryDelayMs ?? 8000;
const maxRetries = args.maxRetries ?? 48;
const retryDelayMs = args.retryDelayMs ?? 5000;
const spentKeys = new Set(spentUtxoRefs.map((r) => `${r.txHash}:${r.outputIndex}`));

for (let attempt = 0; attempt < maxRetries; attempt++) {
Expand All @@ -78,7 +78,7 @@ async function pollUntilUtxosConsumed(args: {
}
}
throw new Error(
`Timed out after ${maxRetries} attempts (${(maxRetries * (args.retryDelayMs ?? 8000)) / 1000}s) waiting for cert tx inputs to be confirmed on-chain`,
`Timed out after ${maxRetries} attempts (${(maxRetries * (args.retryDelayMs ?? 5000)) / 1000}s) waiting for cert tx inputs to be confirmed on-chain`,
);
}

Expand Down Expand Up @@ -527,53 +527,60 @@ export function createScenarioDRepCertificates(): Scenario {
id: "scenario.drep-certificates",
description:
"Register and retire DRep for legacy and SDK wallets, restoring pre-test state",
steps: [
// Legacy: hygiene (deregister if already registered)
createDRepHygieneStep("legacy"),
// Legacy: register
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.legacy.register",
walletType: "legacy",
certEndpoint: "botDRepCertificate",
action: "register",
label: "DRep registration (legacy)",
runtime: legacyReg,
requireBroadcastSuccess: true,
buildExtraBody: () => buildDRepRegBody(),
}),
// Legacy: retire
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.legacy.retire",
walletType: "legacy",
certEndpoint: "botDRepCertificate",
action: "retire",
label: "DRep retirement (legacy)",
runtime: legacyRetire,
requireBroadcastSuccess: true,
}),
// SDK: hygiene (deregister if already registered)
createDRepHygieneStep("sdk"),
// SDK: register
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.sdk.register",
walletType: "sdk",
certEndpoint: "botDRepCertificate",
action: "register",
label: "DRep registration (sdk)",
runtime: sdkReg,
requireBroadcastSuccess: true,
buildExtraBody: () => buildDRepRegBody(),
}),
// SDK: retire
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.sdk.retire",
walletType: "sdk",
certEndpoint: "botDRepCertificate",
action: "retire",
label: "DRep retirement (sdk)",
runtime: sdkRetire,
requireBroadcastSuccess: true,
}),
steps: [],
parallelBranches: [
{
id: "drep-certificates.legacy",
description: "DRep certificate lifecycle (legacy)",
steps: [
createDRepHygieneStep("legacy"),
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.legacy.register",
walletType: "legacy",
certEndpoint: "botDRepCertificate",
action: "register",
label: "DRep registration (legacy)",
runtime: legacyReg,
requireBroadcastSuccess: true,
buildExtraBody: () => buildDRepRegBody(),
}),
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.legacy.retire",
walletType: "legacy",
certEndpoint: "botDRepCertificate",
action: "retire",
label: "DRep retirement (legacy)",
runtime: legacyRetire,
requireBroadcastSuccess: true,
}),
],
},
{
id: "drep-certificates.sdk",
description: "DRep certificate lifecycle (SDK)",
steps: [
createDRepHygieneStep("sdk"),
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.sdk.register",
walletType: "sdk",
certEndpoint: "botDRepCertificate",
action: "register",
label: "DRep registration (sdk)",
runtime: sdkReg,
requireBroadcastSuccess: true,
buildExtraBody: () => buildDRepRegBody(),
}),
...createCertPhaseSteps({
idPrefix: "v1.botDRepCertificate.sdk.retire",
walletType: "sdk",
certEndpoint: "botDRepCertificate",
action: "retire",
label: "DRep retirement (sdk)",
runtime: sdkRetire,
requireBroadcastSuccess: true,
}),
],
},
],
};
}
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci/scenarios/steps/proxyBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ async function pollUntilUtxosConsumed(args: {
maxRetries?: number;
retryDelayMs?: number;
}): Promise<{ attempts: number }> {
const maxRetries = args.maxRetries ?? 30;
const retryDelayMs = args.retryDelayMs ?? 8000;
const maxRetries = args.maxRetries ?? 48;
const retryDelayMs = args.retryDelayMs ?? 5000;
const spent = new Set(args.spentUtxoRefs.map(key));
for (let attempt = 0; attempt < maxRetries; attempt++) {
if (attempt > 0) {
Expand Down
28 changes: 28 additions & 0 deletions src/__tests__/ciScenarioManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ describe("route-chain scenario manifest", () => {
expect(scenarios.map((scenario) => scenario.id)).toEqual(["scenario.create-wallet"]);
});

it("runs legacy and SDK DRep lifecycles as independent parallel branches", () => {
const [scenario] = getScenarioManifest(mkContext(["legacy", "sdk"]), [
"scenario.drep-certificates",
]);

expect(scenario?.steps).toEqual([]);
expect(scenario?.parallelBranches?.map((branch) => branch.id)).toEqual([
"drep-certificates.legacy",
"drep-certificates.sdk",
]);
expect(scenario?.parallelBranches?.[0]?.steps.map((step) => step.id)).toEqual([
"v1.botDRepCertificate.legacy.hygiene",
"v1.botDRepCertificate.legacy.register.propose",
"v1.botDRepCertificate.legacy.register.pending",
"v1.botDRepCertificate.legacy.register.sign.signer1",
"v1.botDRepCertificate.legacy.register.sign.signer2",
"v1.botDRepCertificate.legacy.register.cleared",
"v1.botDRepCertificate.legacy.register.onchain",
"v1.botDRepCertificate.legacy.retire.propose",
"v1.botDRepCertificate.legacy.retire.pending",
"v1.botDRepCertificate.legacy.retire.sign.signer1",
"v1.botDRepCertificate.legacy.retire.sign.signer2",
"v1.botDRepCertificate.legacy.retire.cleared",
"v1.botDRepCertificate.legacy.retire.onchain",
]);
expect(scenario?.parallelBranches?.[1]?.steps).toHaveLength(13);
});

it("still fails clearly when ring transfer is requested without all wallet types", () => {
expect(() =>
getScenarioManifest(mkContext(["legacy"]), ["scenario.real-transfer-and-sign"]),
Expand Down
Loading