diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf57deb1..3385394a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,6 +159,13 @@ jobs: run: npx playwright install --with-deps chromium - name: Exercise real paid browser storage and recovery working-directory: ant-core/browser-tests + # This step compiles only the pinned node, which runs the code of a + # node release so this client is tested against older nodes. Each new + # stable toolchain can add warnings to that old code (Rust 1.99 + # deprecated `fetch_update`), which are not this repository's to fix, + # so they must not fail the build here. + env: + RUSTFLAGS: "" run: npm test test-unit: diff --git a/ant-core/browser-tests/README.md b/ant-core/browser-tests/README.md index 3a619ec0..32a1ebd2 100644 --- a/ant-core/browser-tests/README.md +++ b/ant-core/browser-tests/README.md @@ -53,10 +53,19 @@ The test starts seven real ant-nodes and an isolated Anvil chain, then uses Chromium's real WebRTC implementation. `ANT_NODE_DIR` overrides the default sibling `ant-node-web-support` checkout. CI checks out the exact browser-enabled node revision in [`node-revision`](node-revision); use that revision locally to -reproduce CI. This integration pin is separate from the native `ant-node` -registry dependency, which has no git `rev`. Update the pin only with a passing -paid browser integration run against the replacement. Anvil must be installed -and available on PATH. +reproduce CI. Without `ANT_NODE_DIR`, the devnet refuses to start unless the +sibling checkout is that revision without tracked edits, and builds it with +`--locked`. The pin runs the same code as a node release already on the +network, so the suite checks this client against older nodes. It is currently +`26129a26`, the ant-node `main` commit that adds only the Rust 1.99 fixes to +the 0.21.0 release: its one runtime change renames `fetch_update` to +`try_update`, the same method. CI builds the pin without `-D warnings`, because +each new toolchain can add warnings to an old release. This integration pin is +separate from the native `ant-node` registry dependency, which has no git +`rev`. Update the pin only with a passing paid browser integration run against +the replacement. In CI, the run fails unless every test ran and passed: +`run-guard.js` fails a run with a skipped or unfinished test, and `.only` is +rejected. Anvil must be installed and available on PATH. The harness authenticates through `BrowserNetworkClient.connect()` and retains that pool for the upload, makes one batched storage payment, diff --git a/ant-core/browser-tests/node-revision b/ant-core/browser-tests/node-revision index 5b821f16..8c6eba29 100644 --- a/ant-core/browser-tests/node-revision +++ b/ant-core/browser-tests/node-revision @@ -1 +1 @@ -c092f225877b3058eccbc47e2082fc44750ed2b5 +26129a2611cac08f9f53a557f91fb8806b40a1cc diff --git a/ant-core/browser-tests/playwright.config.js b/ant-core/browser-tests/playwright.config.js index 7d201af7..e67ad957 100644 --- a/ant-core/browser-tests/playwright.config.js +++ b/ant-core/browser-tests/playwright.config.js @@ -1,6 +1,7 @@ import { defineConfig } from "@playwright/test"; export default defineConfig({ - testDir: ".", testMatch: "*.spec.js", workers: 1, timeout: 300_000, reporter: "line", + testDir: ".", testMatch: "*.spec.js", workers: 1, timeout: 300_000, forbidOnly: !!process.env.CI, + reporter: [["line"], ["./run-guard.js"]], use: { browserName: "chromium", headless: true, baseURL: "http://127.0.0.1:35173", launchOptions: { args: ["--force-fieldtrials=WebRTC-NoSdpMangleUfrag/Enabled/"] } }, webServer: [ diff --git a/ant-core/browser-tests/run-guard.js b/ant-core/browser-tests/run-guard.js new file mode 100644 index 00000000..d603b97e --- /dev/null +++ b/ant-core/browser-tests/run-guard.js @@ -0,0 +1,19 @@ +// Fails an otherwise passing run unless every discovered test ran in Chromium and passed. +// Playwright alone exits 0 when tests are skipped, so a skipped test could leave the suite green +// without exercising the pinned nodes. +export default class RunGuard { + onBegin(_, suite) { this.tests = suite.allTests(); } + onEnd(result) { + // `--list` reports the discovered tests without running them, so there is nothing to check. + if (result.status !== "passed" || process.argv.includes("--list")) return; + const tests = this.tests ?? []; + const missed = tests.filter(test => test.results.at(-1)?.status !== "passed"); + if (tests.length > 0 && missed.length === 0) { + console.log(`Run guard: all ${tests.length} tests ran and passed`); + return; + } + console.error(tests.length === 0 ? "Run guard: no tests ran" + : `Run guard: ${missed.length} of ${tests.length} tests did not pass: ${missed.map(test => test.title).join("; ")}`); + return { status: "failed" }; + } +} diff --git a/ant-core/browser-tests/start-devnet.mjs b/ant-core/browser-tests/start-devnet.mjs index 5d712a5d..57ab4c89 100644 --- a/ant-core/browser-tests/start-devnet.mjs +++ b/ant-core/browser-tests/start-devnet.mjs @@ -1,10 +1,29 @@ -import { spawn } from "node:child_process"; -import { mkdtemp, rm } from "node:fs/promises"; +import { execFileSync, spawn } from "node:child_process"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { resolve, join } from "node:path"; -const directory = await mkdtemp(join(tmpdir(), "ant-core-browser-test-")); const node = resolve(process.env.ANT_NODE_DIR ?? "../../../ant-node-web-support"); -const child = spawn("cargo", ["run", "--manifest-path", join(node, "Cargo.toml"), "--bin", "ant-devnet", "--", +const cargo = ["run", "--manifest-path", join(node, "Cargo.toml"), "--bin", "ant-devnet"]; +// Without an explicit ANT_NODE_DIR the suite runs exactly the pinned node: that revision, without tracked edits, +// on its own lockfile. +if (!process.env.ANT_NODE_DIR) { + const pinned = (await readFile("node-revision", "utf8")).trim(); + const git = (...args) => execFileSync("git", ["-C", node, ...args], { encoding: "utf8", stdio: "pipe" }).trim(); + let problem; + try { + const revision = git("rev-parse", "HEAD"); + if (revision !== pinned) problem = `is at ${revision}, not the pinned ${pinned}`; + else if (git("status", "--porcelain", "--untracked-files=no") !== "") problem = `has tracked edits to the pinned ${pinned}`; + } catch { problem = `is not a git checkout of the pinned ${pinned}`; } + if (problem) { + console.error(`${node} ${problem}: check out node-revision there or set ANT_NODE_DIR`); + process.exit(1); + } + console.log(`Starting ant-devnet from the pinned node ${pinned}`); + cargo.push("--locked"); +} +const directory = await mkdtemp(join(tmpdir(), "ant-core-browser-test-")); +const child = spawn("cargo", [...cargo, "--", "--nodes", "7", "--data-dir", directory, "--base-port", "33000", "--webrtc-direct", "--webrtc-direct-base-port", "34000", "--serve-port", "35000", "--enable-evm"], { stdio: "inherit" }); for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill("SIGINT"));