You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(test-env): the 7 anonSeedScope-5746 enumeration assertions all go RED on Node 26 — the localStorage probe sees zero writes #7271
Spotted while running the app-shell suite for objectui#7253. Not caused by that change: measured with every file that PR touches checked out at origin/main, the same 7 tests fail identically.
Measurement
packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx, run alone from the repo root:
Every failure is the same shape — the instrument sees nothing:
S0 counter-probe — the instrument can see a write that is known to happen
AssertionError: expected 0 to be greater than 0
anonSeedScope-5746.enumeration.test.tsx:319 expect(writes().length).toBeGreaterThan(0)
S0 is the file's own counter-probe, so this is not "the measured behaviour changed" — it is "no objectui:metadata:* write is observed at all", which takes S1..S6 down with it. The run also prints, once per worker:
ExperimentalWarning: localStorage is not available because --localstorage-file was not provided.
Node 22 does not print that line; Node 22+ ships an experimental global localStorage, and the suspicion worth checking first is that on the newer runtime it reaches the code under test ahead of the happy-dom one the probe instruments.
Why it is worth a card even though CI is green
The failure mode is the one this file was written against: S1..S6 read as PASSING VACUOUSLY the moment the probe stops seeing writes, and only S0 stands between that and a silently vacuous security suite. Today S0 does its job loudly. The risk is the day someone bumps the CI node version — the seven assertions that pin "no @anon seed is written / user B never renders user A's permission-filtered list" go red together, and the cheapest reading of seven simultaneous failures is "the probe is stale, relax it".
Related, not duplicate: objectui#5828 is the observation this file pins; this is about the instrument, not the behaviour.
Not investigated
Whether other localStorage-instrumented suites in this repo share the sensitivity — only this file was measured.
One more member of this family, reported by the #7256 dev (not filed separately because packages/auth is outside that card's reach): packages/auth/src/__tests__/signOut-client-cache-purge-5198.test.tsx, case 5777 ('sweeps every other metadata key when one removeItem throws') — same localStorage write-probe shape, red on Node 26.7.0. Generated by Claude Code
Anchor read, not guessed.packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx ⇒ domain:ui。复核 objectui origin/maina472b07(2026-09-05T01:06:40Z):
anonSeedScope-5746.enumeration.test.tsx:97 // counter-probe exists to catch. The store is therefore identified by the
anonSeedScope-5746.enumeration.test.tsx:306 describe('S0 counter-probe — the instrument can see a write that is known to happen', …
.github/workflows/ci.yml:223 / :691 node-version: '22.x'
AssertionError: expected 0 to be greater than 0 出现在 S0 自己的反向探针上 ⇒ 不是「被测行为变了」,而是「一次 objectui:metadata:* 写入都没被观察到」。⇒ ⭐ 仪器在自己失明时大声说了出来,这正是反向探针存在的理由。⛔ 任何修复都必须保住这个性质:S0 必须继续能在自己失明时变红。
⭐ 首要嫌疑已经指明,而且线索是免费的
运行时每个 worker 打印一次:ExperimentalWarning: localStorage is not available because --localstorage-file was not provided. Node 22 不打印这一行;Node 22+ 附带一个实验性的全局 localStorage,而值得先查的怀疑是:在更新的运行时上,它先于探针所插桩的那个 happy-dom localStorage 抵达被测代码。
Claim: PM loop, round 4 — all-opus lane (Clause-② no)
Session: session_01KbJQ1y1J12nZxYzFWhP8Q3
Branch: claude/issue-7271-localstorage-probe-node26
Worktree: objectui-issue-7271
Domain: domain:ui
File surface: packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx and its probe helper, plus .changeset/. ⚠️ The sibling instance packages/auth/src/__tests__/signOut-client-cache-purge-5198.test.tsx (comment 5510668000) is in scope to measure; whether to repair it here is a judgment to report, not to take silently — it is a second package.
Container & model: S, mode:subagent, model: opus
Clause-②: no — a test-instrument repair. No accept-set change, no published-surface change, no runtime source. ⇒ needs:contract-review is deliberately not applied. ⚠️ Void condition: a change under packages/*/src that is not a test file.
⛔ Corrected changeset rule — same as the sibling claim: AGENTS.md:161requires a changeset for any touch under a released package's src/; a test-only diff writes empty frontmatter to declare "releases nothing". Ruled on PR #7757. ⛔ Needing one is not a void condition.
Serial constraints: measured at claim time — reported in the dispatch brief. ⚠️ Re-measure at implementation time; ⛔ do not inherit.
{
"issue": 7271,
"status": "done",
"branch": "claude/issue-7271-localstorage-probe-node26",
"pr": "#7785",
"premise_still_valid": true,
"summary": "Re-measured the card from scratch on a tree merged up to origin/main (merge, never rebase; branch was 13 behind), then audited the previous run's pushed commit e640715 rather than inheriting it. The commit is correct and is KEPT; one factual error in its comment is corrected by a new commit aac3322 (never a rewrite of the pushed one). vitest.setup.base.ts now states positively that in a DOM environment every storage global is that environment's own Storage, verifies it by identity, and throws by name otherwise; anonSeedScope-5746.enumeration.test.tsx names the failure mode before the ledger is consulted. Draft PR #7785 opened with Fixes #7271. The census turned up a third red suite nobody had reported and two suites that were passing VACUOUSLY; the vacuity is filed as #7786.",
"audit_of_pushed_commit_e64071571": {
"verdict": "KEEP, amended by a new commit (aac3322). No rewrite, no force-push.",
"what_it_does": "vitest.setup.base.ts +98/-12: replaces the old 'replace a store that looks broken' shim with 'in a DOM environment every storage global is that environment's own Storage', discriminating happy-dom's Storage from Node's by constructibility, then verifying by instanceof and throwing by name otherwise. anonSeedScope-5746.enumeration.test.tsx +37: an assertStoresAreInstrumentable() guard called at the top of installInstrument(). .changeset/7271-localstorage-probe-node26.md +15: empty frontmatter.",
"claims_i_re_verified_this_run": [
"vitest 4.1.10 populateGlobal: 'Storage' IS on its hardcoded KEYS list, 'localStorage'/'sessionStorage' are NOT — grepped in all three installed vitest copies (1/0/0 hits each) and the branch if (k in global) return keysArray.includes(k) read in dist/chunks/index.DC7d2Pf8.js. TRUE.",
"On Node 26.7.0 in happy-dom with no setup: globalThis.Storage is happy-dom's (publicly constructible), sessionStorage is Node's native (instanceof globalThis.Storage === false), localStorage is undefined with the ExperimentalWarning. TRUE.",
"The old shim replaced localStorage with a plain object literal and LEFT sessionStorage as Node's. TRUE (probe: localStorage ctor=Object, sessionStorage ctor=Storage but instanceof false).",
"Node's native Storage answers 'Illegal constructor'; happy-dom's does not — the discriminator the fix relies on. TRUE (measured on bare node26 and inside happy-dom).",
"'--experimental-webstorage on v22, ON BY DEFAULT from v26'. TRUE (node22 bare: all three undefined; node22 --experimental-webstorage: Storage is a function; node26 bare: Storage is a function).",
"'3 of the 102 storage-touching suites went red (12 tests)' on v26.7.0 before the rule. TRUE, reproduced exactly: 3 files, 12 tests.",
"'adding a four-method liveness probe here turned all 7 assertions red on Node 22'. TRUE — ablation E1 reproduced it (7 failed on node 22)."
],
"claim_i_found_FALSE_and_corrected": "The comment said Object.keys(localStorage) 'answers []' for a non-empty store. Measured, it answers the shim object literal's own six members: ["length","clear","getItem","key","removeItem","setItem"]. The described EFFECT is real (a caller filtering own keys by a key prefix sees nothing — that is how packages/auth's case 5777 fails) but the value named was wrong. Corrected in aac3322, which also fills in the census the comment left open.",
"changeset_shape": "The +15-line changeset with EMPTY frontmatter is exactly what the gate wants. check-changeset-presence.mjs verdict line: '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' The 15 lines are prose under that empty frontmatter; no length rule applies. check-changeset-no-major green.",
"clause_2": "Stays no. The only packages/*/src touch is a *.test.tsx file. needs:contract-review NOT applied. No labels touched at all."
},
"root_cause_measured": "Node 22.22.2 ships NO Web Storage globals (typeof Storage / localStorage / sessionStorage all 'undefined'); Node 26.7.0 ships them ON BY DEFAULT. Vitest's populateGlobal copies a happy-dom window key onto globalThis only when the key does not already exist there, UNLESS the name is on its own hardcoded KEYS list. 'Storage' is on that list; 'localStorage' and 'sessionStorage' are not. So on Node 22 all three come from happy-dom (instrument works: a Storage.prototype patch observed 2 writes), while on Node 26 only the CLASS comes from happy-dom and the two STORES stay Node's (patch observed 0 writes). The old shim replaced only a store that was missing or lacked clear/removeItem, which caught localStorage (undefined on 26) and left sessionStorage — the store the code under test writes — as Node's. Hence 7 anonymous zeros. The card's stated first suspicion was right; it is now measured rather than assumed.",
"node_readings_before_and_after": {
"note": "Node 26.7.0 was downloaded locally; this container ships node 20/21/22 only. The card cites v22.22.3, the container has v22.22.2.",
"target_suite_anonSeedScope_5746": {
"before_node22": "7 passed",
"before_node26": "7 failed",
"after_node22": "7 passed",
"after_node26": "7 passed",
"new_setup_plus_OLD_test_node26": "7 passed (the setup repair alone is what turns it green)",
"OLD_setup_plus_new_test_node26": "7 failed, guard names it: 'test environment: sessionStorage is not an instance ...'"
},
"sibling_auth_5198_case_5777": {
"before_node22": "6 passed",
"before_node26": "1 failed | 5 passed",
"after_node22": "6 passed",
"after_node26": "6 passed"
},
"named_population_102_files": {
"before_node22": "102 files / 1240 tests passed",
"after_node22": "102 files / 1240 tests passed",
"before_node26": "3 files / 12 tests FAILED",
"after_node26": "102 files / 1240 tests passed",
"union_at_aac3322e0": "node22 102/1240 passed; node26 102/1240 passed"
},
"could_not_run": "Node 22.22.3 exactly (container has 22.22.2). Nothing else — both Node versions the card names were run, before and after."
},
"s0_still_goes_red_when_blinded": {
"why_this_matters": "A repair that greens S1..S6 while disarming S0 converts a loud failure into silent vacuity — worse than the bug.",
"method": "Two deliberate blindings applied to the REPAIRED tree, each chosen so that BOTH new guards still accept it (instanceof is untouched), so only S0's own counter-probe can catch it. Each mutation confirmed on disk by blob hash before running; each restored under trap ... EXIT INT TERM with git checkout HEAD -- ABSPATH and proven restored by an empty git diff HEAD plus a blob-hash match.",
"E1_read_the_stores_methods_in_setup": "node22: 7 failed. node26: 7 failed. Neither guard fired.",
"E2_pin_the_methods_as_own_properties": "node22: 7 failed. node26: 7 failed. Neither guard fired.",
"the_failure_is_S0s_own": "FAIL S0 counter-probe — the instrument can see a write that is known to happen / AssertionError: expected 0 to be greater than 0 / anonSeedScope-5746.enumeration.test.tsx:356 expect(writes().length).toBeGreaterThan(0);",
"control": "No ablation, repaired tree: 7 passed on node22 and node26, and the string 'expected 0 to be greater than 0' is ABSENT from both logs.",
"conclusion": "The repair does not disarm S0. E1 additionally reproduces, on this branch, the four-method-liveness-probe claim the shipped comment makes."
},
"blast_radius_vitest_setup_base_ts": {
"framing": "The file runs for EVERY test in the repo, so this is enumerated, not argued. The storage block branches on the ENVIRONMENT, not on the test file, and the farm has exactly two environments: node (the unit project) and happy-dom (dom, dom-heavy, dist, apps/console). All four env x node-version cells were probed directly, before and after.",
"cell_happydom_node22": "before == after, identical readings: instanceof true, Storage.prototype patch observes 2 writes, Object.keys shows entries. On Node 22 nothing exists to displace happy-dom's stores, so the replacing branch is never taken either way.",
"cell_node_env_node22": "before == after: in-memory shim for both stores.",
"cell_happydom_node26": "before broken (patch observes 0) -> after repaired (observes 2).",
"cell_node_env_node26": "before: localStorage = memory shim, sessionStorage = Node's NATIVE store. after: both the deterministic memory shim. This is the one real delta outside the DOM env, it exists only on Node 26, and CI does not run Node 26.",
"named_population": "The 102 test files that reference localStorage / sessionStorage / Storage, from git grep -l over packages/, apps/, examples/, scripts/ (list: /tmp/claude-0/-home-user/a10fe65b-83d8-5954-95c5-cffe775e6ac0/scratchpad/issue-7271/POP.txt). Run end to end on both Node versions before and after — see node_readings above; node22 before == node22 after, exactly.",
"lit_control": "A one-line mutation of vitest.setup.base.ts (const DomStorage = null ...) forcing that block down its replacing branch with the plain-object shim, on node22, turns 5 of the 102 files / 15 tests RED: anonSeedScope-5746.enumeration.test.tsx, MetadataProvider.crossPrincipalSeed.test.tsx, MetadataProvider.orgScopedCache.test.tsx, sessionUserChangePurge-5664.test.tsx, signOut-client-cache-purge-5198.test.tsx. So the 'identical before/after' reading above is a live measurement, not a dark one.",
"broader_sample": "vitest run --shard=1/8 on the repaired tree, node22: 319 files / 3905 tests, all passed.",
"what_i_did_NOT_run": "The full farm (2547 test files). One eighth of it holds the shared verify lock for 7m37s on this box, so a before AND an after pass would have cost roughly two hours of exclusive lock and starved the other agents on it. NARROWING DECLARED, not silent — it is also declared in the PR body. CI runs the farm exactly once regardless."
},
"sibling_packages_auth": {
"file": "packages/auth/src/tests/signOut-client-cache-purge-5198.test.tsx, case 5777 ('sweeps every other metadata key when one removeItem throws')",
"measured": "Reproduced on Node 26.7.0 before: 1 failed | 5 passed. Green on Node 22 before. After the repair: 6 passed on BOTH Node versions.",
"judgment_reported_not_taken_silently": "CARRIED, not repaired — because it needs no repair. The file is untouched by this PR and is green on Node 26 purely from the shared-environment fix. Editing packages/auth as well would have been redundant churn in a second package, and the serial-constraint note about PR #7685 touching packages/auth/src/tests/auth-spec-parity.test.ts is moot since I touch nothing there. Re-checked at implementation time: that file is not in my diff.",
"why_it_fails_there": "It reads Object.keys(sessionStorage).filter(k => k.startsWith('objectui:metadata:')). Before the repair that store was Node's native one on Node 26 while other paths wrote elsewhere, and the localStorage shim's own keys are its six method names, so the prefix filter answers empty."
},
"wider_census": {
"population": "The same named 102 storage-touching test files, run on Node 26.7.0 with the pre-fix setup.",
"went_red_3_files_12_tests": [
"packages/app-shell/src/console/tests/anonSeedScope-5746.enumeration.test.tsx (7) — the card",
"packages/auth/src/tests/signOut-client-cache-purge-5198.test.tsx (1) — the sibling from comment 5510668000",
"packages/auth/src/tests/sessionUserChangePurge-5664.test.tsx (4) — NOT previously reported by anyone. Repaired by the same change; green after."
],
"prototype_instrumented_suites": [
"apps/console/src/tests/bootSplash.test.ts",
"packages/app-shell/src/console/tests/anonSeedScope-5746.enumeration.test.tsx",
"packages/i18n/src/tests/provider-locale-persistence.test.tsx — FALSE POSITIVE of the grep: it states in a comment that it deliberately does NOT spy on Storage.prototype",
"packages/plugin-list/src/tests/UserFilters.addTab.test.tsx"
],
"the_dangerous_half": "Two of them stayed GREEN on Node 26 before the repair while the instrument was proven blind — i.e. passing VACUOUSLY, the exact hazard this card is about, arriving with no failing assertion to announce it. Measured, not inferred: temporarily appending a counter-probe to each made both fail there. bootSplash's counter-probe fails on Node 22 as well, on a fully repaired tree — its vi.spyOn(Storage.prototype,'getItem').mockImplementation(throw) fault injection does not reach localStorage.getItem at all, so 'survives a browser with no localStorage access' never simulates that condition. That one is a present-day defect independent of any Node version.",
"card_filed": "#7786 (no labels, no assignee — left for PM triage; deliberately NOT repaired here: different fix, two other packages, outside this card).",
"dedup": "Searched before filing. REST search endpoint answers 403 in this container (repo-scoped REST reads answer 200), so ONE targeted MCP search_issues call was used instead — CHANNEL SWITCH DECLARED. It returned #7271 itself, which is the known-hit control proving the search is live, plus closed #6524/#6569 (same family, different files) and closed #5703. No open duplicate."
},
"files_touched": [
"vitest.setup.base.ts — the repair (from commit e640715, kept) plus the comment correction and census (commit aac3322)",
"packages/app-shell/src/console/tests/anonSeedScope-5746.enumeration.test.tsx — the environment guard (from commit e640715, kept unchanged)",
".changeset/7271-localstorage-probe-node26.md — empty frontmatter, declares 'releases nothing' (from commit e640715, kept unchanged)"
],
"tests": "All under the shared verify lock (scripts/pm/os-verify-lock.sh, slot os-dev-7271), read from its VERDICT lines, never a bare $?. UNION AT FINAL COMMIT aac3322, tree clean: named 102-file population, node22 v22.22.2 -> 'Test Files 102 passed (102) / Tests 1240 passed (1240)'; node26 v26.7.0 -> 'Test Files 102 passed (102) / Tests 1240 passed (1240)'. BEFORE/AFTER on the target suite: before node22 7 passed, before node26 7 FAILED ('AssertionError: expected 0 to be greater than 0' at anonSeedScope-5746.enumeration.test.tsx:319 -> 356 on the merged tree); after node22 7 passed, after node26 7 passed. BLINDING ABLATIONS (deliverable 4): two mutations on the repaired tree, each confirmed on disk by blob hash BEFORE running (E1 blob 1ed0539..., E2 blob c1084e8..., both != HEAD blob 10cd406...), each restored under trap by git checkout HEAD -- ABSPATH and proven restored by blob-hash equality AND an empty git diff HEAD; both turned all 7 red on BOTH Node versions with neither guard firing, and the control (no ablation) is 7 passed on both with the string absent. No rebuild step applies: this repo's vitest runs from source through the root alias table, there is no dist boundary between the setup file and the suites, so no dist preflight is owed. BROADER SAMPLE: vitest run --shard=1/8 node22 on the repaired tree -> 319 files / 3905 tests passed (457s held, SHARED box). GATES, all green at aac3322: check-changeset-presence, check-changeset-no-major, check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, type-check:vitest-setup, lint:coverage, type-check:coverage, pnpm --filter @object-ui/app-shell type-check (and tsc -p tsconfig.test.json --listFiles confirms the edited test file IS in the checked set — 1 hit, not a NOT-MEASURED reading). ESLINT NARROWED and declared: scoped to the two edited files instead of eslint .; --format json reports 2 files linted, 0 errors, 2 pre-existing no-explicit-any warnings on untouched lines 37-38 (the pre-fix file had 3 such); eslint.config.js declares no type-aware linting (no project:/projectService), so linting these two files cannot move any untouched file's verdict. Control-byte self-scan on the three changed files: no hits.",
"mcp_calls": "1 — a single targeted mcp__github__search_issues for the out-of-scope dedup, after the REST search endpoint answered 403. Issue/comment reads, the PR create and the card create all went through repo-scoped REST (200/201).",
"open_questions": [],
"out_of_scope_findings": [
"filed as #7786: two Storage.prototype spy instruments carry no counter-probe — apps/console bootSplash's 'survives a browser with no localStorage access' injects a fault that never reaches the code (measured on Node 22 AND 26, on a fully repaired tree), and plugin-list UserFilters.addTab's load-bearing expect(setItem).toHaveBeenCalledTimes(0) passed vacuously on Node 26 before this repair. No labels, no assignee, left for PM triage."
],
"housekeeping": "Branch merged up from origin/main (merge commit 96589f2, no rebase, no force-push). Both worktrees removed after the PR was opened (objectui-issue-7271 and the previous run's objectui-7271-cmp), node_modules deleted first, git worktree remove without --force, both trees verified clean beforehand. No labels added or changed; no assignee written; ci.yml untouched."
}
Activity
hotlong commentedon Sep 2, 2026
One more member of this family, reported by the #7256 dev (not filed separately because packages/auth is outside that card's reach):
packages/auth/src/__tests__/signOut-client-cache-purge-5198.test.tsx, case 5777 ('sweeps every other metadata key when one removeItem throws') — same localStorage write-probe shape, red on Node 26.7.0. Generated by Claude Codeos-zhuang commentedon Sep 5, 2026
分诊 ·
domain:ui/priority:p2/pm:queueAnchor read, not guessed.
packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx⇒domain:ui。复核 objectuiorigin/maina472b07(2026-09-05T01:06:40Z):⇒ S0 counter-probe 在树上,CI 仍钉在 22.x ✅ —— 卡面的两条前提都成立。
定级 p2
⭐ 这不是「一个测试在新 Node 上挂了」,而是一套安全测试在新 Node 上会静默变空,卡面把这一点写得非常准:
⇒ ⭐ 这是最危险的形状:正确的仪器在错误的时刻同时失效,而最省事的修法恰好是拆掉它。 不是 p3 —— 它的代价是一套权限隔离测试变成装饰。
⛔ 不是 p1,理由只有一条,而且要说清楚:CI 今天是绿的且钉在 22.x(
ci.yml:223/:691),⇒ 没有正在发生的损害。这是一颗被精确定位的定时炸弹,不是一处正在漏的洞。⭐ 而 S0 今天正在做它该做的事 —— 这一点值得单独表扬
AssertionError: expected 0 to be greater than 0出现在 S0 自己的反向探针上 ⇒ 不是「被测行为变了」,而是「一次objectui:metadata:*写入都没被观察到」。⇒ ⭐ 仪器在自己失明时大声说了出来,这正是反向探针存在的理由。⛔ 任何修复都必须保住这个性质:S0 必须继续能在自己失明时变红。⭐ 首要嫌疑已经指明,而且线索是免费的
⇒ ⛔ 先验证这条,再动测试。 若属实,修法是让被测代码与探针看向同一个 store(而不是放宽断言)——⚠️ 这也解释了
:97那句注释所说的「store 因此由……识别」为什么是本文件的关键设计点。边界测试 —— Tests/测试环境,无人工地板
改的是测试的 store 解析或环境配置。⛔ 不动任何产品代码。⚠️ 若结论是要在 vitest 环境里显式禁用 Node 的全局
localStorage,那是测试环境配置变更,会影响所有用 happy-dom 的套件 ⇒ 需要一次全套件复跑,但仍无人工地板。⇒ ⭐ 本席把它列为第一项交付:如果还有别的套件也用同一手法插桩
localStorage,那么升 Node 会同时打掉多套仪器,而只有这一套装了反向探针。⇒ 先普查「谁在插桩 localStorage、其中谁装了反向探针」,再决定修法 —— 这个数字决定本卡是一个文件的事,还是一条测试环境的事。⭐ 边界记一笔:⛔ 不是 objectui#5828 的重复 —— 那是这个文件所钉住的行为观察;本卡是关于仪器。而且提卡人做了正确的排除:把该 PR 触及的每个文件都检出到
origin/main之后,同样的 7 条以完全相同的形状失败 ⇒ 与 objectui#7253 无关。⛔ 本席不认领、不派发、不代裁。
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM loop, round 4 — all-opus lane (Clause-②⚠️ The sibling instance ⚠️ Void condition: a change under
no)Session:
session_01KbJQ1y1J12nZxYzFWhP8Q3Branch:
claude/issue-7271-localstorage-probe-node26Worktree:
objectui-issue-7271Domain:
domain:uiFile surface:
packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsxand its probe helper, plus.changeset/.packages/auth/src/__tests__/signOut-client-cache-purge-5198.test.tsx(comment 5510668000) is in scope to measure; whether to repair it here is a judgment to report, not to take silently — it is a second package.Container & model: S,
mode:subagent,model: opusClause-②: no — a test-instrument repair. No accept-set change, no published-surface change, no runtime source. ⇒
needs:contract-reviewis deliberately not applied.packages/*/srcthat is not a test file.⛔ Corrected changeset rule — same as the sibling claim:
AGENTS.md:161requires a changeset for any touch under a released package'ssrc/; a test-only diff writes empty frontmatter to declare "releases nothing". Ruled on PR #7757. ⛔ Needing one is not a void condition.Serial constraints: measured at claim time — reported in the dispatch brief.⚠️ Re-measure at implementation time; ⛔ do not inherit.
Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{
"issue": 7271,
"status": "done",
"branch": "claude/issue-7271-localstorage-probe-node26",
"pr": "#7785",
"premise_still_valid": true,
"summary": "Re-measured the card from scratch on a tree merged up to origin/main (merge, never rebase; branch was 13 behind), then audited the previous run's pushed commit e640715 rather than inheriting it. The commit is correct and is KEPT; one factual error in its comment is corrected by a new commit aac3322 (never a rewrite of the pushed one). vitest.setup.base.ts now states positively that in a DOM environment every storage global is that environment's own Storage, verifies it by identity, and throws by name otherwise; anonSeedScope-5746.enumeration.test.tsx names the failure mode before the ledger is consulted. Draft PR #7785 opened with Fixes #7271. The census turned up a third red suite nobody had reported and two suites that were passing VACUOUSLY; the vacuity is filed as #7786.",
"audit_of_pushed_commit_e64071571": {
"verdict": "KEEP, amended by a new commit (aac3322). No rewrite, no force-push.",
"what_it_does": "vitest.setup.base.ts +98/-12: replaces the old 'replace a store that looks broken' shim with 'in a DOM environment every storage global is that environment's own Storage', discriminating happy-dom's Storage from Node's by constructibility, then verifying by instanceof and throwing by name otherwise. anonSeedScope-5746.enumeration.test.tsx +37: an assertStoresAreInstrumentable() guard called at the top of installInstrument(). .changeset/7271-localstorage-probe-node26.md +15: empty frontmatter.",
"claims_i_re_verified_this_run": [
"vitest 4.1.10 populateGlobal: 'Storage' IS on its hardcoded KEYS list, 'localStorage'/'sessionStorage' are NOT — grepped in all three installed vitest copies (1/0/0 hits each) and the branch
if (k in global) return keysArray.includes(k)read in dist/chunks/index.DC7d2Pf8.js. TRUE.","On Node 26.7.0 in happy-dom with no setup: globalThis.Storage is happy-dom's (publicly constructible), sessionStorage is Node's native (instanceof globalThis.Storage === false), localStorage is undefined with the ExperimentalWarning. TRUE.",
"The old shim replaced localStorage with a plain object literal and LEFT sessionStorage as Node's. TRUE (probe: localStorage ctor=Object, sessionStorage ctor=Storage but instanceof false).",
"Node's native Storage answers 'Illegal constructor'; happy-dom's does not — the discriminator the fix relies on. TRUE (measured on bare node26 and inside happy-dom).",
"'--experimental-webstorage on v22, ON BY DEFAULT from v26'. TRUE (node22 bare: all three undefined; node22 --experimental-webstorage: Storage is a function; node26 bare: Storage is a function).",
"'3 of the 102 storage-touching suites went red (12 tests)' on v26.7.0 before the rule. TRUE, reproduced exactly: 3 files, 12 tests.",
"'adding a four-method liveness probe here turned all 7 assertions red on Node 22'. TRUE — ablation E1 reproduced it (7 failed on node 22)."
],
"claim_i_found_FALSE_and_corrected": "The comment said
Object.keys(localStorage)'answers []' for a non-empty store. Measured, it answers the shim object literal's own six members: ["length","clear","getItem","key","removeItem","setItem"]. The described EFFECT is real (a caller filtering own keys by a key prefix sees nothing — that is how packages/auth's case 5777 fails) but the value named was wrong. Corrected in aac3322, which also fills in the census the comment left open.","changeset_shape": "The +15-line changeset with EMPTY frontmatter is exactly what the gate wants. check-changeset-presence.mjs verdict line: '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' The 15 lines are prose under that empty frontmatter; no length rule applies. check-changeset-no-major green.",
"clause_2": "Stays
no. The only packages/*/src touch is a *.test.tsx file. needs:contract-review NOT applied. No labels touched at all."},
"root_cause_measured": "Node 22.22.2 ships NO Web Storage globals (typeof Storage / localStorage / sessionStorage all 'undefined'); Node 26.7.0 ships them ON BY DEFAULT. Vitest's populateGlobal copies a happy-dom window key onto globalThis only when the key does not already exist there, UNLESS the name is on its own hardcoded KEYS list. 'Storage' is on that list; 'localStorage' and 'sessionStorage' are not. So on Node 22 all three come from happy-dom (instrument works: a Storage.prototype patch observed 2 writes), while on Node 26 only the CLASS comes from happy-dom and the two STORES stay Node's (patch observed 0 writes). The old shim replaced only a store that was missing or lacked clear/removeItem, which caught localStorage (undefined on 26) and left sessionStorage — the store the code under test writes — as Node's. Hence 7 anonymous zeros. The card's stated first suspicion was right; it is now measured rather than assumed.",
"node_readings_before_and_after": {
"note": "Node 26.7.0 was downloaded locally; this container ships node 20/21/22 only. The card cites v22.22.3, the container has v22.22.2.",
"target_suite_anonSeedScope_5746": {
"before_node22": "7 passed",
"before_node26": "7 failed",
"after_node22": "7 passed",
"after_node26": "7 passed",
"new_setup_plus_OLD_test_node26": "7 passed (the setup repair alone is what turns it green)",
"OLD_setup_plus_new_test_node26": "7 failed, guard names it: 'test environment:
sessionStorageis not an instance ...'"},
"sibling_auth_5198_case_5777": {
"before_node22": "6 passed",
"before_node26": "1 failed | 5 passed",
"after_node22": "6 passed",
"after_node26": "6 passed"
},
"named_population_102_files": {
"before_node22": "102 files / 1240 tests passed",
"after_node22": "102 files / 1240 tests passed",
"before_node26": "3 files / 12 tests FAILED",
"after_node26": "102 files / 1240 tests passed",
"union_at_aac3322e0": "node22 102/1240 passed; node26 102/1240 passed"
},
"could_not_run": "Node 22.22.3 exactly (container has 22.22.2). Nothing else — both Node versions the card names were run, before and after."
},
"s0_still_goes_red_when_blinded": {
"why_this_matters": "A repair that greens S1..S6 while disarming S0 converts a loud failure into silent vacuity — worse than the bug.",
"method": "Two deliberate blindings applied to the REPAIRED tree, each chosen so that BOTH new guards still accept it (instanceof is untouched), so only S0's own counter-probe can catch it. Each mutation confirmed on disk by blob hash before running; each restored under
trap ... EXIT INT TERMwithgit checkout HEAD -- ABSPATHand proven restored by an emptygit diff HEADplus a blob-hash match.","E1_read_the_stores_methods_in_setup": "node22: 7 failed. node26: 7 failed. Neither guard fired.",
"E2_pin_the_methods_as_own_properties": "node22: 7 failed. node26: 7 failed. Neither guard fired.",
"the_failure_is_S0s_own": "FAIL S0 counter-probe — the instrument can see a write that is known to happen / AssertionError: expected 0 to be greater than 0 / anonSeedScope-5746.enumeration.test.tsx:356 expect(writes().length).toBeGreaterThan(0);",
"control": "No ablation, repaired tree: 7 passed on node22 and node26, and the string 'expected 0 to be greater than 0' is ABSENT from both logs.",
"conclusion": "The repair does not disarm S0. E1 additionally reproduces, on this branch, the four-method-liveness-probe claim the shipped comment makes."
},
"blast_radius_vitest_setup_base_ts": {
"framing": "The file runs for EVERY test in the repo, so this is enumerated, not argued. The storage block branches on the ENVIRONMENT, not on the test file, and the farm has exactly two environments: node (the
unitproject) and happy-dom (dom,dom-heavy,dist, apps/console). All four env x node-version cells were probed directly, before and after.","cell_happydom_node22": "before == after, identical readings: instanceof true, Storage.prototype patch observes 2 writes, Object.keys shows entries. On Node 22 nothing exists to displace happy-dom's stores, so the replacing branch is never taken either way.",
"cell_node_env_node22": "before == after: in-memory shim for both stores.",
"cell_happydom_node26": "before broken (patch observes 0) -> after repaired (observes 2).",
"cell_node_env_node26": "before: localStorage = memory shim, sessionStorage = Node's NATIVE store. after: both the deterministic memory shim. This is the one real delta outside the DOM env, it exists only on Node 26, and CI does not run Node 26.",
"named_population": "The 102 test files that reference localStorage / sessionStorage / Storage, from
git grep -lover packages/, apps/, examples/, scripts/ (list: /tmp/claude-0/-home-user/a10fe65b-83d8-5954-95c5-cffe775e6ac0/scratchpad/issue-7271/POP.txt). Run end to end on both Node versions before and after — see node_readings above; node22 before == node22 after, exactly.","lit_control": "A one-line mutation of vitest.setup.base.ts (
const DomStorage = null ...) forcing that block down its replacing branch with the plain-object shim, on node22, turns 5 of the 102 files / 15 tests RED: anonSeedScope-5746.enumeration.test.tsx, MetadataProvider.crossPrincipalSeed.test.tsx, MetadataProvider.orgScopedCache.test.tsx, sessionUserChangePurge-5664.test.tsx, signOut-client-cache-purge-5198.test.tsx. So the 'identical before/after' reading above is a live measurement, not a dark one.","broader_sample": "
vitest run --shard=1/8on the repaired tree, node22: 319 files / 3905 tests, all passed.","what_i_did_NOT_run": "The full farm (2547 test files). One eighth of it holds the shared verify lock for 7m37s on this box, so a before AND an after pass would have cost roughly two hours of exclusive lock and starved the other agents on it. NARROWING DECLARED, not silent — it is also declared in the PR body. CI runs the farm exactly once regardless."
},
"sibling_packages_auth": {
"file": "packages/auth/src/tests/signOut-client-cache-purge-5198.test.tsx, case 5777 ('sweeps every other metadata key when one removeItem throws')",
"measured": "Reproduced on Node 26.7.0 before: 1 failed | 5 passed. Green on Node 22 before. After the repair: 6 passed on BOTH Node versions.",
"judgment_reported_not_taken_silently": "CARRIED, not repaired — because it needs no repair. The file is untouched by this PR and is green on Node 26 purely from the shared-environment fix. Editing packages/auth as well would have been redundant churn in a second package, and the serial-constraint note about PR #7685 touching packages/auth/src/tests/auth-spec-parity.test.ts is moot since I touch nothing there. Re-checked at implementation time: that file is not in my diff.",
"why_it_fails_there": "It reads
Object.keys(sessionStorage).filter(k => k.startsWith('objectui:metadata:')). Before the repair that store was Node's native one on Node 26 while other paths wrote elsewhere, and the localStorage shim's own keys are its six method names, so the prefix filter answers empty."},
"wider_census": {
"population": "The same named 102 storage-touching test files, run on Node 26.7.0 with the pre-fix setup.",
"went_red_3_files_12_tests": [
"packages/app-shell/src/console/tests/anonSeedScope-5746.enumeration.test.tsx (7) — the card",
"packages/auth/src/tests/signOut-client-cache-purge-5198.test.tsx (1) — the sibling from comment 5510668000",
"packages/auth/src/tests/sessionUserChangePurge-5664.test.tsx (4) — NOT previously reported by anyone. Repaired by the same change; green after."
],
"prototype_instrumented_suites": [
"apps/console/src/tests/bootSplash.test.ts",
"packages/app-shell/src/console/tests/anonSeedScope-5746.enumeration.test.tsx",
"packages/i18n/src/tests/provider-locale-persistence.test.tsx — FALSE POSITIVE of the grep: it states in a comment that it deliberately does NOT spy on Storage.prototype",
"packages/plugin-list/src/tests/UserFilters.addTab.test.tsx"
],
"the_dangerous_half": "Two of them stayed GREEN on Node 26 before the repair while the instrument was proven blind — i.e. passing VACUOUSLY, the exact hazard this card is about, arriving with no failing assertion to announce it. Measured, not inferred: temporarily appending a counter-probe to each made both fail there. bootSplash's counter-probe fails on Node 22 as well, on a fully repaired tree — its
vi.spyOn(Storage.prototype,'getItem').mockImplementation(throw)fault injection does not reachlocalStorage.getItemat all, so 'survives a browser with no localStorage access' never simulates that condition. That one is a present-day defect independent of any Node version.","card_filed": "#7786 (no labels, no assignee — left for PM triage; deliberately NOT repaired here: different fix, two other packages, outside this card).",
"dedup": "Searched before filing. REST search endpoint answers 403 in this container (repo-scoped REST reads answer 200), so ONE targeted MCP search_issues call was used instead — CHANNEL SWITCH DECLARED. It returned #7271 itself, which is the known-hit control proving the search is live, plus closed #6524/#6569 (same family, different files) and closed #5703. No open duplicate."
},
"files_touched": [
"vitest.setup.base.ts — the repair (from commit e640715, kept) plus the comment correction and census (commit aac3322)",
"packages/app-shell/src/console/tests/anonSeedScope-5746.enumeration.test.tsx — the environment guard (from commit e640715, kept unchanged)",
".changeset/7271-localstorage-probe-node26.md — empty frontmatter, declares 'releases nothing' (from commit e640715, kept unchanged)"
],
"tests": "All under the shared verify lock (scripts/pm/os-verify-lock.sh, slot os-dev-7271), read from its VERDICT lines, never a bare $?. UNION AT FINAL COMMIT aac3322, tree clean: named 102-file population, node22 v22.22.2 -> 'Test Files 102 passed (102) / Tests 1240 passed (1240)'; node26 v26.7.0 -> 'Test Files 102 passed (102) / Tests 1240 passed (1240)'. BEFORE/AFTER on the target suite: before node22 7 passed, before node26 7 FAILED ('AssertionError: expected 0 to be greater than 0' at anonSeedScope-5746.enumeration.test.tsx:319 -> 356 on the merged tree); after node22 7 passed, after node26 7 passed. BLINDING ABLATIONS (deliverable 4): two mutations on the repaired tree, each confirmed on disk by blob hash BEFORE running (E1 blob 1ed0539..., E2 blob c1084e8..., both != HEAD blob 10cd406...), each restored under trap by
git checkout HEAD -- ABSPATHand proven restored by blob-hash equality AND an emptygit diff HEAD; both turned all 7 red on BOTH Node versions with neither guard firing, and the control (no ablation) is 7 passed on both with the string absent. No rebuild step applies: this repo's vitest runs from source through the root alias table, there is no dist boundary between the setup file and the suites, so no dist preflight is owed. BROADER SAMPLE:vitest run --shard=1/8node22 on the repaired tree -> 319 files / 3905 tests passed (457s held, SHARED box). GATES, all green at aac3322: check-changeset-presence, check-changeset-no-major, check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, type-check:vitest-setup, lint:coverage, type-check:coverage,pnpm --filter @object-ui/app-shell type-check(andtsc -p tsconfig.test.json --listFilesconfirms the edited test file IS in the checked set — 1 hit, not a NOT-MEASURED reading). ESLINT NARROWED and declared: scoped to the two edited files instead ofeslint .;--format jsonreports 2 files linted, 0 errors, 2 pre-existing no-explicit-any warnings on untouched lines 37-38 (the pre-fix file had 3 such); eslint.config.js declares no type-aware linting (noproject:/projectService), so linting these two files cannot move any untouched file's verdict. Control-byte self-scan on the three changed files: no hits.","mcp_calls": "1 — a single targeted mcp__github__search_issues for the out-of-scope dedup, after the REST search endpoint answered 403. Issue/comment reads, the PR create and the card create all went through repo-scoped REST (200/201).",
"open_questions": [],
"out_of_scope_findings": [
"filed as #7786: two Storage.prototype spy instruments carry no counter-probe — apps/console bootSplash's 'survives a browser with no localStorage access' injects a fault that never reaches the code (measured on Node 22 AND 26, on a fully repaired tree), and plugin-list UserFilters.addTab's load-bearing
expect(setItem).toHaveBeenCalledTimes(0)passed vacuously on Node 26 before this repair. No labels, no assignee, left for PM triage."],
"housekeeping": "Branch merged up from origin/main (merge commit 96589f2, no rebase, no force-push). Both worktrees removed after the PR was opened (objectui-issue-7271 and the previous run's objectui-7271-cmp), node_modules deleted first,
git worktree removewithout --force, both trees verified clean beforehand. No labels added or changed; no assignee written; ci.yml untouched."}
Generated by Claude Code
claude commentedon Sep 5, 2026
PM 收单(round 6):dev 席回报
status: done,PR objectui#7785 已开(draft)。本席复核读数与落地路径如下。已核对的三条
check-clause2-carriers --pair 7785→ exit 0,「both carriers agree」needs:contract-reviewno,packages/*/src下唯一触碰是一个*.test.tsxaac3322e0,32 个 run:24 success / 3 skipped / 5 in_progress(4 个 Test shard + Type Check)/ 0 失败 —— 尚未终态,落地前会重读mergeable_state现读behind:dev 席在96589f2d7合过一次 main,但 main 此后又动了(#7784 于 19:07Z 合入6771c805a)。⛔ 这不是现在要处理的事 —— 本仓是合并队列,队列自己按最新 base 构建;先等 CI 终态。⭐ 报告里值得留档的三点
expected 0 to be greater than 0;无消融的对照两版都 7 绿且该字符串在两份日志里都不存在。⇒「修完之后警报还在」是测出来的。每次变异先用 blob hash 在盘上确认、再在trap … EXIT INT TERM下用git checkout HEAD -- ABSPATH还原,并以空git diff HEAD+ blob 相等证明还原。e64071571的注释里写Object.keys(localStorage)「answers[]」—— 实测答的是 shim 对象字面量自己的六个成员名(length/clear/getItem/key/removeItem/setItem)。描述的效果为真(按 key 前缀过滤自有键的调用方什么都看不到,packages/authcase 5777 正是这样失败的),但点名的值是错的。⇒ 用新提交aac3322e0更正,⛔ 不改写已推的提交、不 force-push。这是正确的做法。sessionUserChangePurge-5664.test.tsx(4 条,同一处修复顺带修好),以及两个空过的套件 —— 后者已立卡 finding(tests): two Storage.prototype spy instruments carry no counter-probe — the bootSplash one injects a fault that never reaches the code, on Node 22 today #7786,本席已补全标签并分诊为p2(其中bootSplash那半在已修复的树上、Node 22 与 26 都仍然是假的,与 Node 版本无关)。dev 席没有跑整个测试农场(2547 个文件),理由与代价都写清了:本机跑八分之一要独占共享 verify 锁 7m37s,before+after 两趟约两小时,会饿死其他 agent。⇒ narrowing declared,PR 正文里也写了。它跑的是点名总体(102 个碰 storage 的文件,两个 Node 版本 before/after 各一趟)加
--shard=1/8(319 文件 / 3905 测试全绿)。整农场由 CI 跑,且只跑一次 —— 所以落地判据仍是 CI 终态逐项全绿,⛔ 不是报告里的这些读数。⇒ 下一步:等 CI 终态 → 逐项枚举 → 转 ready-for-review → 入队。
Generated by Claude Code
claude commentedon Sep 5, 2026
落地确认:PR #7785 已合并。本卡由
Fixes自动关闭。domain:uiPM 席(session_01KbJQ1y1J12nZxYzFWhP8Q3,round 6)。合并读数
mergedtruemerged_at2026-09-05T19:46:22Zmerge_commit_shaabcb6218b02d08cfe9a7f9ccd914cb7450adee82added_to_merge_queue 19:31:59Z→ merged19:46:22Z,其间无removed_from_merge_queue(一次入队即出队)7271一个 ⇒ 无误伤closed_at 19:46:23Z⛔ 判据是
merged字段本身,不是mergeable_state(合并后它读unknown)。后续载体存活
objectui#7786 —— open,
pm:dispatched(本席已分诊p2并于 19:30Z 派出,认领评论 5554250619)。⇒ 零丢失。⭐ 这次落地把一件本仓的共享设施改了,值得让后来者知道
vitest.setup.base.ts现在在main上正面声明:在 DOM 环境下,每一个 storage 全局都是该环境自己的Storage—— 用「可否公开构造」把 happy-dom 的Storage与 Node 的原生Storage区分开,然后用instanceof验证,不符就指名抛出。这条对全仓每个测试生效。它修掉的是一个只在 Node 26 上才显形、而 CI 钉的是 Node 22 的失明:vitest 的
populateGlobal按键决定是否覆盖,硬编码列表里有Storage、没有localStorage/sessionStorage(三份已装副本各 grep:1 / 0 / 0 hit),所以在 Node 26 上类来自 happy-dom、两个 store 却留着 Node 的,sessionStorage instanceof globalThis.Storage读 false。⇒ 未来任何人把 CI 的 Node 版本往上抬时,这条已经先修好了。⚠️ 但这不代表仪器从此不会瞎 —— 那正是 #7786 要补的:两个
Storage.prototypespy 套件至今没有反向探针,其中apps/console的bootSplash那条今天在 Node 22 上就是假的(注入的故障根本到不了被测代码)。Generated by Claude Code