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); + } +}