From a54a292d62cce68421de4456303e2b9178393c94 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 18:19:44 +0100 Subject: [PATCH] test(ui): fix Node 25+ localStorage crashes in jsdom suites; document PR-open CI drops (#110812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(ui): shim dead Node 25+ WebStorage localStorage in jsdom test setup Node 25+ ships a default-on global localStorage that is dead without --localstorage-file (undefined on 26.5; throwing or inert elsewhere). It shadows jsdom's Storage during vitest global population, so storage-touching UI tests crash on newer local Node while Linux CI (Node 24) passes — six files failed locally on Node 26.5. The shared setup now capability-probes (round-trip, throw-safe), prefers jsdom's own window Storage when only the global alias is dead, and otherwise installs one in-memory Storage on both window and globalThis. Also documents the PR-open CI drop pattern in AGENTS.md: fresh PRs race GitHub's merge-ref computation and the open-event CI run can drop or die as startup_failure/BuildFailed (not rerunnable); verify attach and close/reopen to re-fire. * fix(test): drop unnecessary String() conversions in localStorage shim --- AGENTS.md | 1 + ui/src/test-helpers/lit-warnings.setup.ts | 67 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5529a783d12a..4b8714bedef7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -232,6 +232,7 @@ Skills own workflows; root owns hard policy and routing. - No surprise GH writes: chat must mention every posted/updated public comment with URL. - GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`. - PR create: real body required. Use the current template: `What Problem This Solves`, `Why This Change Was Made`, `User Impact`, and `Evidence`; include visible refs, behavior, and validation. +- PR create races GitHub's merge-ref computation: the pull_request-open CI run can drop entirely or die as `startup_failure`/`BuildFailed` (`(Unknown event)`, not rerunnable). After opening, verify the CI workflow attached to the head SHA; if missing, close/reopen the PR to re-fire the event. - PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe. - GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available. - Contributor PRs: parsed context requires authored `What Problem This Solves` and `Evidence` sections. Do not require field-level proof forms; reviewers inspect code, tests, and CI for correctness. diff --git a/ui/src/test-helpers/lit-warnings.setup.ts b/ui/src/test-helpers/lit-warnings.setup.ts index e0babec91397..ffe1fe0fcda7 100644 --- a/ui/src/test-helpers/lit-warnings.setup.ts +++ b/ui/src/test-helpers/lit-warnings.setup.ts @@ -77,3 +77,70 @@ if (typeof HTMLDialogElement !== "undefined" && !("close" in HTMLDialogElement.p }, }); } + +// Node 25+ enables WebStorage by default with a global localStorage getter +// that is dead without --localstorage-file (undefined on 26.5, reported to +// throw or return an inert proxy on other 25/26 releases). During jsdom +// global population it shadows the DOM Storage (globalThis is the window), +// so storage-touching tests crash on newer local Node while Linux CI +// (Node 24, no default WebStorage) passes. Capability-probe instead of +// trusting any one shape, then install an in-memory Storage. +function globalLocalStorageIsUsable(): boolean { + try { + const existing = globalThis.localStorage; + if (!existing) { + return false; + } + existing.setItem("__openclaw_probe__", "1"); + const roundTrips = existing.getItem("__openclaw_probe__") === "1"; + existing.removeItem("__openclaw_probe__"); + return roundTrips; + } catch { + return false; + } +} + +function usableWindowLocalStorage(): Storage | null { + try { + const candidate = window.localStorage; + if (!candidate) { + return null; + } + candidate.setItem("__openclaw_probe__", "1"); + const roundTrips = candidate.getItem("__openclaw_probe__") === "1"; + candidate.removeItem("__openclaw_probe__"); + return roundTrips ? candidate : null; + } catch { + return null; + } +} + +if (typeof window !== "undefined" && !globalLocalStorageIsUsable()) { + const backing = new Map(); + // Prefer jsdom's own Storage when only the global alias is dead so + // `localStorage` and `window.localStorage` stay the same object. + const storage: Storage = usableWindowLocalStorage() ?? { + get length() { + return backing.size; + }, + clear: () => backing.clear(), + getItem: (key: string) => backing.get(key) ?? null, + key: (index: number) => [...backing.keys()][index] ?? null, + removeItem: (key: string) => { + backing.delete(key); + }, + setItem: (key: string, value: string) => { + backing.set(key, value); + }, + }; + const install = (target: object) => + Object.defineProperty(target, "localStorage", { + configurable: true, + enumerable: false, + get: () => storage, + }); + install(globalThis); + if ((window as unknown) !== globalThis) { + install(window); + } +}