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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(plugin-view): two more render-inside-waitFor predicates — bounded today only by an RTL implementation detail #7802
{
"issue": 7802,
"status": "done",
"branch": "claude/issue-7802-objectview-waitfor-render",
"pr": "https://github.com/objectstack-ai/objectui/pull/7808",
"premise_still_valid": true,
"summary": "Both render-bearing waitFor predicates are gone. 6726:103 and 6840:132 now assert on `expect(await deliveredThrough(asData)).toHaveLength(2)` directly — the shape the other two cases in each file already had — so no waitFor predicate in either file renders. The card's premise held on origin/main: both files, both line numbers, both the async waitFor(async () => ...) construct, exactly as described. ONE correction to the card's wording, which changed nothing: `deliveredThrough` is NOT a shared module. Each file declares its own private copy (6726:75, 6840:104), neither is exported, and git grep returns 8 lines all inside these two files (2 declarations + 6 call sites) — so the 'stop and report if it has other callers' branch was checked and is not reached, and no helper was duplicated to dodge the question. SECOND CHANGE, in the same declared file surface and needed by the first: removing the outer waitFor also removes the RETRY that was masking an imprecise readiness signal, so the signal was measured rather than assumed — it does not hold. ObjectView hands the child an empty rows array three times before the fetch lands (measured delivery sequence [0,0,0,2]), so `delivered.length > 0` is satisfied by an EMPTY delivery; with find() deferred 50/200/500ms the old readiness returns [] every time, and the asRecords/asValue refusal pins pass VACUOUSLY (they read the initial empty delivery before an answer exists, so they would stay green even if the component did read that key). The helper now awaits find()'s own settled answer first (`await ds.find.mock.results[0].value`) — a pure read of the mock's call record, no DOM touched. ObjectView.tsx untouched, no case deleted/skipped/weakened, the async was not dropped anywhere it still guards a rendering predicate. Clause-2 void condition NOT reached: GitHub changed_files = 3 = two test files plus .changeset/eighty-pandas-listen.md.",
"tests": "All readings at 558084dee; exit codes captured before any pipe. PROBE (interval pinned AT timeout 2000/2000, cap 200 runs), measured on this card's own helper: sync render-into-document 200 runs/191ms/body-growth 200; sync render into a DETACHED container 1 run/2006ms/growth 0; inert 1 run/2002ms/growth 0; the async shape these files had 1 run/2003ms/growth 1; the same async shape at the default 50ms interval 19 runs/2006ms/growth 19 — growth equals run count exactly in both async rows (one leaked container div per run). LIT CONTROL — 6726:103 mutated async to synchronous, everything else unchanged. Mutation proved ON DISK before each run: bare-anchor count 1 to 0, mutated-anchor count 0 to 1, blob hash 46d4e6401 to c0527a102. Outcomes: default threads pool at 4096MB — NO test output at all, killed at the 480s wall (exit 124); --pool=forks at 4096MB — same, exit 124. REPORTED HONESTLY: neither 4096MB run reached the allocation limit inside the wall, so neither log carries V8 frames or a tests line and NEITHER IS QUOTED for them. Lowering the ceiling to 512MB with --pool=forks reached it in 26.84s, exit 1, and captured the full evidence: 'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory', AllocateRawWithRetryOrFailSlowPath, Runtime_AllocateInYoungGeneration, 'Worker exited unexpectedly', 'Test Files (1) / Tests (3) / Errors 1 error', 'Duration 26.84s (... tests 0ms ...)'. forks was used because under the default pool the reporter dies with the worker (measured on 7756). The same file unmutated spends 508ms of tests time. RESTORATION proved BY STATE, not by whether the trap fired: blob hash back to 46d4e6401 AND `git diff HEAD` for that path empty (0 bytes). 2 of 2. DEFERRAL TABLE (2 envelopes x 4 deferrals x 2 readiness variants): asData wants 2 — old readiness returns 2 at 0ms but 0 at 50/200/500ms (3 FAIL); settled readiness returns 2 at all four. asRecords wants 0 — both return 0 at all four, but 'old' does so vacuously. ACCEPTANCE — the predicate no longer mutates the DOM, measured two ways: (1) instrumented, 50 consecutive runs of the repaired helper made 150 waitFor predicate invocations with document.body growth INSIDE those predicates = 0, all 50 returning the two rows; (2) mechanical, a repo-wide scan of 2571 test files (brace-balanced, following same-file helpers transitively) for render( reachable from inside a waitFor predicate now reports 2 hits, BOTH in plugin-detail/RelatedList.lookupLabelResolution.test.tsx — that is 7756's file, PR #7801, not merged and not touched. Both plugin-view files are clear; the same scan before this change reported those two plus these two. GREEN: `vitest run` both files — Test Files 2 passed (2), Tests 6 passed (6), tests 508ms, exit 0. `vitest run packages/plugin-view/` — Test Files 34 passed (34), Tests 297 passed (297), exit 0. `turbo run type-check --filter @object-ui/plugin-view` — 16 successful, 16 total, exit 0 (through turbo, not a bare tsc); MEASURED not assumed: `tsc -p tsconfig.test.json --listFiles` confirms both edited files are program inputs, 1 hit each. `turbo run lint --filter @object-ui/plugin-view` — 2 successful, 2 total, exit 0; 48 files, 0 errors, 308 warnings, the two edited files contributing 3 each, all @typescript-eslint/no-explicit-any in the style the files already used — the `any` count is unchanged in both (4 before, 4 after), so no warning was added. GATES: check:control-bytes OK (6357 tracked text files); check:vi-mock-specifiers OK; check:vi-mock-inherit OK (129 call sites on @object-ui/react judged, 129 inherit, 0 auto-mocked); check-changeset-no-major.mjs — 'No changeset declares a major bump.' Plus a manual control-byte grep over the three changed files: no hits. DECLARED NARROWING: lint ran 2 of the workspace lint tasks. eslint.config.js declares no type-aware parser options (0 occurrences of projectService / parserOptions / project:) and each package lints its own directory, so a change confined to two test files in plugin-view cannot move an untouched package's verdict. CI runs the full farm regardless. NOT RUN, said plainly: the full `pnpm test` farm and the repo-wide `pnpm lint` were not run locally; CI owns them. CHANGESET GATE verdict verbatim, exit 0: 'Compared the working tree with 9ae1e9d95 (merge-base with origin/main): 2 file(s) changed, 2 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 1 changeset(s) added.' then: 'OK 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/eighty-pandas-listen.md. Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' Acted on: empty-frontmatter changeset written, never major. The skip-changeset label was NOT applied — in this repo that label is inert.",
"mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probed with GET first, HTTP 200); no MCP GitHub tool was called",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
claude commentedon Sep 5, 2026
Claim: PM loop, round 6 — all-opus lane (Clause-②⚠️ Void condition: 任何 ⚠️ 但存在读耦合的反面:本卡的修法照抄 #7801 的形状,所以先
no)Session:
session_01KbJQ1y1J12nZxYzFWhP8Q3Branch:
claude/issue-7802-objectview-waitfor-renderWorktree:
objectui-issue-7802Domain:
domain:uiFile surface:
packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx·packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6840.test.tsx· 两者共用的那个会render的 helper(若它在别处、且被别的文件用到,先上报再动)· 本卡自己的.changeset/*.md. ⛔ Stop on breach: 任何packages/plugin-view/src/下非测试文件的改动、任何packages/plugin-detail/的改动(那是 #7756 的面,PR #7801 在队列中)。Explain in the report.Container & model: S,
mode:subagent,model: opusClause-②: no — 两个测试文件的等待方式。不改变契约接受/拒绝行为,不扩大公开面。
packages/*/src下非测试文件的改动。Serial constraints cleared: PR #7801(卡 #7756)在队列中,改的是
packages/plugin-detail/src/__tests__/RelatedList.lookupLabelResolution.test.tsx—— 不同包、不同文件,与本卡不相交。git fetch origin main看它落了没有;落了就直接读它的最终形态,没落就读 PR #7801 的 diff。⛔ 无论如何不要改它。在飞 #7799(16 个测试的 cwd 路径)与 #7753(新门)都与本卡不相交。缺陷 ——⚠️ 它今天不炸,而这正是立卡的理由
两处
render出现在waitFor谓词里:packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsxpackages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6840.test.tsx两处都是
waitFor(async () => expect(await deliveredThrough(asData))…),helper 会render。⭐ 它们不是 #7756 那个堆炸弹 —— 这是测出来的(四臂探针,读数在 PR #7801 正文里):
async形状实测 40 runs / 2003ms,正好是 50ms 间隔的节拍 ⇒ DOM 变动根本没有重入它。原因在 RTL 的wait-for.js:checkCallback在promiseStatus === 'pending'时提前返回。⇒ 每次重跑仍泄漏一个节点(RTL 的
unmount()拆掉 React root 却留下 append 的容器 div),但被超时封顶 —— 有界泄漏,不是堆炸弹。⛔ 所以这张卡的价值全在这一句
checkCallback对 pending promise 的提前返回是 RTL 的实现,不是它承诺的行为。⇒ RTL 改掉它、或者有人把谓词从async改成同步,这两个文件立刻变成 #7756 那个杀 worker 的形状。今天不炸是因为一个意外,不是因为一条保证。修法
与 #7756 / PR #7801 同形:先等一个无副作用的就绪条件,再渲染一次并断言同样的文本。判据:谓词不再变动 DOM。
⛔ 三条禁止:
async改成同步来"简化" —— 那会摘掉今天唯一挡住它的那道意外屏障,把有界泄漏变成堆炸弹。ObjectView的运行期源码。若发现被测代码真的有缺陷,立卡,不要顺手修。deliveredThrough)被两个文件共用。先查它还有没有别的调用方;若有,改它可能影响面外文件 —— 那时停下上报,⛔ 不要为了省事把 helper 改成两份。⭐ 验收 —— 必须证明「不再是自我喂养」,⛔ 不是"改完还绿"
「改完还绿」证明不了任何东西:这两个文件今天本来就是绿的。要的是机制层的读数:
interval钉在timeout上(使间隔计时器不可能重跑谓词),记录谓词重跑次数与document.body增长。async临时改成同步(保持其余不变),确认它变成 finding(plugin-detail): RelatedList.lookupLabelResolution renders inside a waitFor predicate — a 50ms lookup-batch delay exhausts the V8 heap and kills the whole vitest worker #7756 那个形状(重跑次数暴涨 / 堆增长失控 / worker 死);然后在trap … EXIT INT TERM下用git checkout HEAD -- <绝对路径>还原,以 blob hash 相等 + 空git diff HEAD证明 —— ⛔ 证明靠状态,不靠 trap 是否触发;变异前先用 hash 确认落盘(空 hash 读作失败)。--pool=forks跑并说明原因 —— finding(plugin-detail): RelatedList.lookupLabelResolution renders inside a waitFor predicate — a 50ms lookup-batch delay exhausts the V8 heap and kills the whole vitest worker #7756 的席位实测:默认池下 reporter 随 worker 一起死,V8 帧与tests 0ms都捕不到,只有 forks 能捕到。⛔ 不要从捕不到的那次引用那两条证据。packages/plugin-view/的常规绿读数,报出文件数与用例数。变更集
改动落在发版包
src/下 ⇒ 按AGENTS.md:161大概率需要。⛔ 不要凭规则推断:跑node scripts/check-changeset-presence.mjs,判词原样抄进报告,按判词行事;欠就写空 frontmatter。⛔ 永不写major。tsc --noEmit不是本仓的门 —— turbotype-checkdependsOn ^build且到不了非包目录;TS2882是未满足的前提,不是结果,⛔ 不要当结果报。POST是真写入。探通道只用GET。scripts/__tests__/check-doc-links.test.ts共享盒上 15s 超时是已知 flake。new URL(rel, import.meta.url)被 Vite 重写成http://localhost:3000/@fs/…,两种 cwd 下fileURLToPath都抛。路径派生用从import.meta.url逐段上溯。origin/main..HEAD报「本 PR 改了几个文件」 —— 本仓并发落地,origin/main会在你跑的过程中前进,那个 diff 会把姊妹 PR 已合并的工作算到你头上(今天实测虚报成 4 个文件,真实 2 个)。以记录的 base 或 GitHub 的changed_files为准。契约复审
Clause-② 是
no⇒ 不排在契约复审后面。Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 7802, "status": "done", "branch": "claude/issue-7802-objectview-waitfor-render", "pr": "https://github.com/objectstack-ai/objectui/pull/7808", "premise_still_valid": true, "summary": "Both render-bearing waitFor predicates are gone. 6726:103 and 6840:132 now assert on `expect(await deliveredThrough(asData)).toHaveLength(2)` directly — the shape the other two cases in each file already had — so no waitFor predicate in either file renders. The card's premise held on origin/main: both files, both line numbers, both the async waitFor(async () => ...) construct, exactly as described. ONE correction to the card's wording, which changed nothing: `deliveredThrough` is NOT a shared module. Each file declares its own private copy (6726:75, 6840:104), neither is exported, and git grep returns 8 lines all inside these two files (2 declarations + 6 call sites) — so the 'stop and report if it has other callers' branch was checked and is not reached, and no helper was duplicated to dodge the question. SECOND CHANGE, in the same declared file surface and needed by the first: removing the outer waitFor also removes the RETRY that was masking an imprecise readiness signal, so the signal was measured rather than assumed — it does not hold. ObjectView hands the child an empty rows array three times before the fetch lands (measured delivery sequence [0,0,0,2]), so `delivered.length > 0` is satisfied by an EMPTY delivery; with find() deferred 50/200/500ms the old readiness returns [] every time, and the asRecords/asValue refusal pins pass VACUOUSLY (they read the initial empty delivery before an answer exists, so they would stay green even if the component did read that key). The helper now awaits find()'s own settled answer first (`await ds.find.mock.results[0].value`) — a pure read of the mock's call record, no DOM touched. ObjectView.tsx untouched, no case deleted/skipped/weakened, the async was not dropped anywhere it still guards a rendering predicate. Clause-2 void condition NOT reached: GitHub changed_files = 3 = two test files plus .changeset/eighty-pandas-listen.md.", "tests": "All readings at 558084dee; exit codes captured before any pipe. PROBE (interval pinned AT timeout 2000/2000, cap 200 runs), measured on this card's own helper: sync render-into-document 200 runs/191ms/body-growth 200; sync render into a DETACHED container 1 run/2006ms/growth 0; inert 1 run/2002ms/growth 0; the async shape these files had 1 run/2003ms/growth 1; the same async shape at the default 50ms interval 19 runs/2006ms/growth 19 — growth equals run count exactly in both async rows (one leaked container div per run). LIT CONTROL — 6726:103 mutated async to synchronous, everything else unchanged. Mutation proved ON DISK before each run: bare-anchor count 1 to 0, mutated-anchor count 0 to 1, blob hash 46d4e6401 to c0527a102. Outcomes: default threads pool at 4096MB — NO test output at all, killed at the 480s wall (exit 124); --pool=forks at 4096MB — same, exit 124. REPORTED HONESTLY: neither 4096MB run reached the allocation limit inside the wall, so neither log carries V8 frames or a tests line and NEITHER IS QUOTED for them. Lowering the ceiling to 512MB with --pool=forks reached it in 26.84s, exit 1, and captured the full evidence: 'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory', AllocateRawWithRetryOrFailSlowPath, Runtime_AllocateInYoungGeneration, 'Worker exited unexpectedly', 'Test Files (1) / Tests (3) / Errors 1 error', 'Duration 26.84s (... tests 0ms ...)'. forks was used because under the default pool the reporter dies with the worker (measured on 7756). The same file unmutated spends 508ms of tests time. RESTORATION proved BY STATE, not by whether the trap fired: blob hash back to 46d4e6401 AND `git diff HEAD` for that path empty (0 bytes). 2 of 2. DEFERRAL TABLE (2 envelopes x 4 deferrals x 2 readiness variants): asData wants 2 — old readiness returns 2 at 0ms but 0 at 50/200/500ms (3 FAIL); settled readiness returns 2 at all four. asRecords wants 0 — both return 0 at all four, but 'old' does so vacuously. ACCEPTANCE — the predicate no longer mutates the DOM, measured two ways: (1) instrumented, 50 consecutive runs of the repaired helper made 150 waitFor predicate invocations with document.body growth INSIDE those predicates = 0, all 50 returning the two rows; (2) mechanical, a repo-wide scan of 2571 test files (brace-balanced, following same-file helpers transitively) for render( reachable from inside a waitFor predicate now reports 2 hits, BOTH in plugin-detail/RelatedList.lookupLabelResolution.test.tsx — that is 7756's file, PR #7801, not merged and not touched. Both plugin-view files are clear; the same scan before this change reported those two plus these two. GREEN: `vitest run` both files — Test Files 2 passed (2), Tests 6 passed (6), tests 508ms, exit 0. `vitest run packages/plugin-view/` — Test Files 34 passed (34), Tests 297 passed (297), exit 0. `turbo run type-check --filter @object-ui/plugin-view` — 16 successful, 16 total, exit 0 (through turbo, not a bare tsc); MEASURED not assumed: `tsc -p tsconfig.test.json --listFiles` confirms both edited files are program inputs, 1 hit each. `turbo run lint --filter @object-ui/plugin-view` — 2 successful, 2 total, exit 0; 48 files, 0 errors, 308 warnings, the two edited files contributing 3 each, all @typescript-eslint/no-explicit-any in the style the files already used — the `any` count is unchanged in both (4 before, 4 after), so no warning was added. GATES: check:control-bytes OK (6357 tracked text files); check:vi-mock-specifiers OK; check:vi-mock-inherit OK (129 call sites on @object-ui/react judged, 129 inherit, 0 auto-mocked); check-changeset-no-major.mjs — 'No changeset declares a major bump.' Plus a manual control-byte grep over the three changed files: no hits. DECLARED NARROWING: lint ran 2 of the workspace lint tasks. eslint.config.js declares no type-aware parser options (0 occurrences of projectService / parserOptions / project:) and each package lints its own directory, so a change confined to two test files in plugin-view cannot move an untouched package's verdict. CI runs the full farm regardless. NOT RUN, said plainly: the full `pnpm test` farm and the repo-wide `pnpm lint` were not run locally; CI owns them. CHANGESET GATE verdict verbatim, exit 0: 'Compared the working tree with 9ae1e9d95 (merge-base with origin/main): 2 file(s) changed, 2 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 1 changeset(s) added.' then: 'OK 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/eighty-pandas-listen.md. Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' Acted on: empty-frontmatter changeset written, never major. The skip-changeset label was NOT applied — in this repo that label is inert.", "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probed with GET first, HTTP 200); no MCP GitHub tool was called", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 5, 2026
落地记录 —— PR #7808 已合入(本 lane 今日第 21 次落地)
merged: true@ 2026-09-05T23:31:33Z,squashc8e4a52f2#7802closed_at23:31:34Z⭐ 四条值得留下来的读数
1. 机制表 —— 把"为什么今天没炸"量成了数字。
五个 arm,
interval钉在timeout上(2000/2000),所以 interval 计时器永远不可能重跑谓词,第一次之后的每一次都是 MutationObserver 驱动的:document.body增长async—— 这两个文件原本的形状async形状、默认 50ms interval⇒
async形状不自喂(wait-for.js的checkCallback在promiseStatus === 'pending'时提前返回,DOM 变更无法重入一个仍在飞的谓词),但它每跑一次泄漏一个节点 —— 增长数与重跑次数逐行相等。⭐ 卡片说"今天只被 RTL 的实现细节兜住",这张表就是那句话的读数:兜住它的是超时,不是契约。2. ⭐⭐ 点灯对照 —— 那个"顺手简化"真的会杀掉整个 shard。
把
6726:103的谓词从async改成同步(正是卡片警告的那次"simplification"):exit 124)--pool=forks--pool=forksexit 1,26.84s⭐⚠️ 而执行席如实报告了 4096MB 那两次并没有到达分配上限、只是被
tests 0ms—— 一条测试都没被报告过;死的是整个 shard,不是一个用例。 同一个文件不变异时是 508ms。timeout杀掉,所以它们不携带 V8 栈帧、也没有tests行,因而不被引用为证据 —— 这是一次干净的"不夸大自己的读数"。3. ⭐⭐ 卡片没要、但修复必须要的第二个读数:外层
waitFor一直在掩盖一个不精确的就绪信号。拿掉外层
waitFor就等于拿掉了一次重试,于是那个就绪信号必须被检验而不是假定。它不成立:ObjectView的 rows 初始为空数组,在 fetch 落地前把data={[]}交给子组件三次 ——asData的实测投递序列是[0, 0, 0, 2]。⇒
await waitFor(() => expect(delivered.length).toBeGreaterThan(0))可以被一次空投递满足,helper 之所以拿到对的行,只是因为 RTL 的 act-flush 恰好先处理了setData。把
find()的答复延迟 0/50/200/500ms 各测一遍(2 信封 × 4 延迟 × 2 就绪写法),⭐asRecords那一列才是更锋利的一半:naive 写法在每个延迟下都"通过",但是空过 —— 它在答案存在之前就读到了初始的空投递,所以即使组件真的去读了records,那条拒绝钉子也会保持绿色。4. 验收性质用两种方式量了。 仪器法:修复后的 helper 连跑 50 次共 150 次谓词调用,谓词内
document.body增长 0。机械法:全仓扫描 2571 个测试文件(括号配平、跨同文件 helper 传递追踪)找"waitFor谓词里可达render(",现在报 2 处,都在 objectui#7756 的文件里(其修复 PR #7801 尚未合入);改动前这个扫描报的是那两处加上本卡这两处。Generated by Claude Code