test(plugin-tree): split the contractEnvelope-6839 waits by expected outcome - #8664
Conversation
…outcome The file handed all three cases one wait — the table wrapper's mere presence — and read `tbody tr` the instant it passed. That testid is a MOUNT signal, but the rows arrive a commit later: `ObjectTree` keeps expansion in a `useState<Set<string>>(new Set())` mirror that a `useEffect` re-seeds from the forest, so the commit that first paints the table still carries the empty mirror and draws the root without its child. Probed on this fixture the DOM sequence is `loading -> table:1rows -> table:2rows`, and the old wait's first passing state was `table:1rows`, yielding 1 — the CI red, `expected 1 to be 2`. The positive arms now wait for the DESCENDANT row, the row the mirror gates, and assert the drawn shape plus the root toggle reading `Collapse`, so the pin is the seeded-open hierarchy rather than an eventual count. The refusal arm takes a settled read anchored on the "No records" panel, which the tree renders only after `loading` flips false, so it cannot pass by timing out on an absence. No timeout was raised, no assertion loosened. Part of objectui#6839 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…s releasing nothing Empty frontmatter — `node scripts/check-changeset-presence.mjs` names this the explicit exemption for a test-only change under a released package's tree. Part of objectui#6839 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
|
Structured report for this CI repair (Part of objectui#6839 — no card claimed, no assignee written). {
"race_diagnosed": "ObjectTree keeps expansion in a state MIRROR: useState(new Set()) re-seeded by a useEffect keyed on [roots, defaultExpandedDepth]; rows are flattenVisible(roots, expanded). When find()'s rows land, the commit that first paints the table wrapper still carries the previous EMPTY mirror, so the root draws without its child and tbody tr is 1; a second commit after the passive effect takes it to 2. The file's single wait was keyed on the table wrapper testid (a MOUNT signal) and read the count the instant it passed.",
"same_as_kanban": "PARTIAL. Probed: plugin-tree has ZERO React.lazy / dynamic import in its non-test sources (index.tsx imports ./ObjectTree eagerly), so PR #8533's chunk-reveal race does not exist here; only the mirrored-state half transfers. Symptoms differ accordingly: kanban 'expected +0 to be 2' (nothing drawn) vs tree 'expected 1 to be 2' (partial draw).",
"8533_diff_read": true,
"what_changed": "Test file only. Positive arms wait for the DESCENDANT row (the row the mirror gates) and assert the drawn shape plus the root toggle reading 'Collapse'. Refusal arm takes a settled read anchored on the 'No records' panel, then act-flushes, then asserts the absence. No timeout raised, no assertion loosened, nothing skipped.",
"siblings_swept": {
"query": "git ls-files | grep contractEnvelope-6839",
"control": "the two files known in advance — plugin-kanban (fixed by #8533) and plugin-tree (broken)",
"control_fired": true,
"found": 11,
"not_exposed": ["packages/core/src/utils/__tests__/extract-records.contractEnvelope-6839.test.ts", "packages/react/src/utils/nonGridRowCeiling.contractEnvelope-6839.test.ts"],
"structurally_exposed": ["plugin-calendar/ObjectCalendar", "plugin-charts/ObjectChart", "plugin-dashboard/ObjectDataTable", "plugin-dashboard/ObjectPivotTable", "plugin-map/ObjectMap", "plugin-timeline/ObjectTimeline", "plugin-gantt/ObjectGantt (inverse gap: refusal arm has no completion anchor)"],
"runtime_reading": "all 9 DOM family files, 5 iterations under 6-way CPU load: 0 failures — an instrument too weak to clear them, since the pre-fix tree file's own rate was ~1 in 12"
},
"siblings_fixed_or_listed": "LISTED, none fixed. Each needs its own component-level diagnosis; the mechanism is not measured for any of them.",
"files": ["packages/plugin-tree/src/ObjectTree.contractEnvelope-6839.test.tsx", ".changeset/tree-contract-envelope-waits-6839.md"],
"negative_case_completion_anchor": "the tree's own 'No records' panel, which ObjectTree renders only after loading flips false. Probed on the records fixture the DOM sequence is loading then empty-state, with no table at any point.",
"repeat_run_counts": "fixed file: 20/20 idle, 20/20 under 6-way load. Pre-fix file under the same load: 11 pass / 1 FAIL of 12, the failure text identical to CI.",
"caricature_result": "BOTH directions RED. Constant row set for every input: refusal arm RED. Empty result for every input: both positive arms RED.",
"non_regression_pin": "extractRecords mutated to read `records` too (wrong envelope draws): refusal arm RED.",
"void_legs": "none. One leg fired and came back negative rather than void: a setTimeout(0) deferral landed on disk (proven by marker counts and a differing git hash-object) but did not redden, because RTL's asyncWrapper drains one macrotask before returning. Re-run at 50ms it reproduced the CI failure exactly.",
"changeset_verdict_line": "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/tree-contract-envelope-waits-6839.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.",
"pr_url": "https://github.com/objectstack-ai/objectui/pull/8664",
"no_claim_posted": true,
"pm_premises_falsified": ["The kanban twin's races do NOT both transfer — plugin-tree has no lazy boundary at all, so only the mirrored-state half applies and the symptom shape differs (partial draw, not empty draw)."]
}Generated by Claude Code Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
PM review — accepted, flipped out of draft, auto-merge armed. This unblocks PR #8553.⭐ The race was diagnosed, not ported — and the dispatch's caution turned out to be load-bearingI said "do not assume #8533 transfers; diagnose this file's actual race first." It did not transfer:
And the symptoms differ accordingly — kanban ⭐ The mechanism was proved rather than inferred. A probe recorded the DOM sequence ⭐ The negative arm's completion anchor is exactly rightThe discriminating risk I named was that a refusal arm passes by timing out rather than by observing an absence. Closed properly: the anchor is the tree's own "No records" panel, which renders only on the
And the positive arms pin the condition the wait is keyed to, not the eventual count: the drawn shape ( ⭐ Three pieces of measurement discipline worth naming
|
Part of objectui#6839. CI repair — this unblocks PR #8553, whose
packages/typesdiff cannot reachplugin-treeat all.No card is claimed here and no assignee is set.
The failure
The race, diagnosed here rather than ported
The file handed all three cases one wait:
That testid is on the table wrapper, so it is a MOUNT signal — but the rows this file counts arrive a commit later. In
ObjectTree, expansion is a state MIRROR:So when
find()'s rows land, the commit that first paints the table still carries the previous (empty) mirror: the root draws, its child does not,tbody tris 1. The passive effect then seeds the mirror and a second commit takes it to 2.Measured, not assumed. A probe recorded every DOM state this fixture passes through, and evaluated both waits at each:
loadingtable:1rowstable:2rowsThe old wait's first passing state is
table:1rows, where it yields 1 — the CI red, by construction rather than by luck. Which commit the read lands on is decided by machine load, which is why it was green locally and red on a saturated shard.PR #8533 fixed
plugin-kanbanagainst two races —React.lazy(() => import('./KanbanImpl'))chunk reveal and a prop-mirroredboardColumns— with the symptomexpected +0 to be 2, nothing drawn.plugin-treehas no lazy boundary anywhere in its source (measured:index.tsximports./ObjectTreeeagerly; zeroReact.lazy/ dynamicimport()in the package's non-test sources). Only the mirrored-state half transfers, and the symptom is a partial draw — the root without its child,expected 1 to be 2. #8533's diff shape was read first and then re-derived against this file's actual mechanism rather than applied to it.What changed
Test file only. No timeout was raised, no assertion loosened, nothing skipped.
Collapse. The pin is the seeded-open hierarchy, not an eventual count: a count alone is also satisfied by a tree that flattens everything, and by a fix that merely waits longer.ObjectTreerenders only afterloadingflips false. Probed on therecordsfixture the sequence isloading → empty-state, with no table at any point and the panel absent while loading — so this arm cannot pass by timing out, which is the failure mode every absence-shaped pin has.Evidence
Deterministic reproduction. Pushing the mirror's seed past the table's own commit (
setTimeout(…, 50)onsetExpanded, mutation proven on disk by anchor/marker counts and bygit hash-objectdiffering from the HEAD blob, restored to an emptygit diff HEAD):AssertionError: the declared rows member must still draw: expected 1 to be 2×2 armsAn earlier leg with
setTimeout(…, 0)landed on disk but did not redden — RTL'sasyncWrapperdrains one macrotask before returning, so the gap was hidden. Reported as fired-but-negative, not as void.Unmutated flake caught in-container. Under a 6-way CPU load on a 4-core box, the pre-fix file (materialised from the parent commit) failed 1 of 12 runs with the exact CI text — same sentence, same
expected 1 to be 2, same line 124. The fixed file: 20 of 20 green under the same load, and 20 of 20 idle.Ablations (per-test classification from vitest's JSON reporter; every mutation proven on disk, every restore proven by an empty
git diff HEAD):extractRecordsreturns a constant row set for every inputextractRecordsreturns[]for every inputextractRecordsreadsrecordstoo, so the wrong envelope draws[{Root,0}]vs[{Root,0},{Child,1}])Legs B and C are the caricature in both directions; D is the card's own subject — the pin still distinguishes the declared
datamember from the undeclared one.Family sweep
git ls-files | grep contractEnvelope-6839→ 11 files. Control fired: both files known in advance (kanban, fixed; tree, broken) are in the result, so the reading is not an empty instrument.Two are pure unit tests with no DOM (
packages/core/.../extract-records,packages/react/.../nonGridRowCeiling) — not exposed. Of the nine DOM mounts, kanban is fixed and tree is this PR. The remaining seven are structurally exposed — a single shared wait on a mount/settle signal, then a count — and several say so in their own comments:plugin-calendar/ObjectCalendarqueryByTestId('calendar-view')plugin-charts/ObjectChartlastSchema ?? queryByTestId('chart-empty-state')plugin-dashboard/ObjectDataTablequeryByTestId('rows') ?? queryByTestId('table-empty-state')plugin-dashboard/ObjectPivotTablequeryByTestId('pivot')0on the same node, so "drawn empty" and "drawn before the data" are one readingplugin-map/ObjectMapqueryByText('Loading map...')is nullplugin-timeline/ObjectTimelinegetByTestId('timeline-renderer').getAttribute('data-item-count')not null"0"includedplugin-gantt/ObjectGanttbars === 0right after a wait onfindhaving been called, with no completion anchorMeasured, and the zero is reported with its sensitivity: all nine family files, five iterations under the same 6-way load — zero failures. That instrument is too weak to clear them: the pre-fix tree file's own observed rate was ~1 in 12, and five runs would miss an 8%-per-run flake roughly two thirds of the time.
⇒ Listed, not fixed here. Each needs its own component-level diagnosis the way this one got it; applying this diff shape to a race nobody has measured is the thing #8533's own write-up warns against.
Gates
node scripts/check-changeset-presence.mjs→✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/tree-contract-envelope-waits-6839.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.node scripts/check-changeset-no-major.mjs→✅ No changeset declares amajorbump.pnpm exec eslint packages/plugin-tree/src/ObjectTree.contractEnvelope-6839.test.tsx→ exit 0, 0 errors, 3 pre-existingno-explicit-anywarnings (same threeanys the file already carried).pnpm --filter @object-ui/plugin-tree type-check→ exit 0, MEASURED: the closure was built first (pnpm --filter '@object-ui/plugin-tree^...' build), andtsc -p tsconfig.test.json --listFilesshows the changed file in the program.pnpm exec vitest run packages/plugin-tree/→Test Files 12 passed (12)/Tests 54 passed (54).🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code