Skip to content

Commit 5900c1c

Browse files
committed
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.
1 parent 3e8f1c6 commit 5900c1c

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ Skills own workflows; root owns hard policy and routing.
232232
- No surprise GH writes: chat must mention every posted/updated public comment with URL.
233233
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
234234
- 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.
235+
- 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.
235236
- 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.
236237
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
237238
- 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.

ui/src/test-helpers/lit-warnings.setup.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,70 @@ if (typeof HTMLDialogElement !== "undefined" && !("close" in HTMLDialogElement.p
7777
},
7878
});
7979
}
80+
81+
// Node 25+ enables WebStorage by default with a global localStorage getter
82+
// that is dead without --localstorage-file (undefined on 26.5, reported to
83+
// throw or return an inert proxy on other 25/26 releases). During jsdom
84+
// global population it shadows the DOM Storage (globalThis is the window),
85+
// so storage-touching tests crash on newer local Node while Linux CI
86+
// (Node 24, no default WebStorage) passes. Capability-probe instead of
87+
// trusting any one shape, then install an in-memory Storage.
88+
function globalLocalStorageIsUsable(): boolean {
89+
try {
90+
const existing = globalThis.localStorage;
91+
if (!existing) {
92+
return false;
93+
}
94+
existing.setItem("__openclaw_probe__", "1");
95+
const roundTrips = existing.getItem("__openclaw_probe__") === "1";
96+
existing.removeItem("__openclaw_probe__");
97+
return roundTrips;
98+
} catch {
99+
return false;
100+
}
101+
}
102+
103+
function usableWindowLocalStorage(): Storage | null {
104+
try {
105+
const candidate = window.localStorage;
106+
if (!candidate) {
107+
return null;
108+
}
109+
candidate.setItem("__openclaw_probe__", "1");
110+
const roundTrips = candidate.getItem("__openclaw_probe__") === "1";
111+
candidate.removeItem("__openclaw_probe__");
112+
return roundTrips ? candidate : null;
113+
} catch {
114+
return null;
115+
}
116+
}
117+
118+
if (typeof window !== "undefined" && !globalLocalStorageIsUsable()) {
119+
const backing = new Map<string, string>();
120+
// Prefer jsdom's own Storage when only the global alias is dead so
121+
// `localStorage` and `window.localStorage` stay the same object.
122+
const storage: Storage = usableWindowLocalStorage() ?? {
123+
get length() {
124+
return backing.size;
125+
},
126+
clear: () => backing.clear(),
127+
getItem: (key: string) => backing.get(String(key)) ?? null,
128+
key: (index: number) => [...backing.keys()][index] ?? null,
129+
removeItem: (key: string) => {
130+
backing.delete(String(key));
131+
},
132+
setItem: (key: string, value: string) => {
133+
backing.set(String(key), String(value));
134+
},
135+
};
136+
const install = (target: object) =>
137+
Object.defineProperty(target, "localStorage", {
138+
configurable: true,
139+
enumerable: false,
140+
get: () => storage,
141+
});
142+
install(globalThis);
143+
if ((window as unknown) !== globalThis) {
144+
install(window);
145+
}
146+
}

0 commit comments

Comments
 (0)