test(gantt): anchor both contractEnvelope-6839 refusal arms on completion, and diagnose the six siblings - #8707
Merged
Conversation
…tion (objectui#8665) Both refusal arms in `ObjectGantt.contractEnvelope-6839.test.tsx` read without a completion anchor. Probed on the component with the row query and the quick-filter domain query answering at different times: - sink 2 (the option domain) asserted an ABSENCE with nothing proving the domain query had answered. With the LIVE `data` envelope and the domain answering one macrotask after the rows, the arm read `offersP3=false, offered=[p1]` while the same mount settled to `offered=[p1,p2,p3]`. The refusal assertion passed against a component that demonstrably resolved the whole domain. - sink 1 read the bars straight after a wait on `find` merely having been CALLED. Under the same profile the DOM at that moment was the `Loading Gantt chart...` placeholder and the read threw `Unable to find an element by: [data-testid="gantt-view"]`. sink 1 now waits for `gantt-view`, which renders below `ObjectGantt`'s `if (loading)` early return; `loading` is cleared in `reload`'s `finally`, in the same `isCurrent()`-guarded commit as `setData`, so its presence proves the row query settled and committed. sink 2 anchors on a CONTROL DIMENSION. The component publishes no DOM marker for the domain fetch on the refusing branch, so the fixture adds a second lookup (`owner`) resolved by the same effect: `setLookupOptions` is called once after the loop over every dimension, and no loaded task carries an `owner`, so its option can only come from the domain fetch having been read and committed. Both anchors are PRESENCE assertions, so neither arm can pass by timing out. The positive arms now also assert the drawn titles, not only a count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
… nothing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Contributor
✅ 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
|
os-justin
marked this pull request as ready for review
September 8, 2026 23:54
os-justin
enabled auto-merge
September 8, 2026 23:54
This was referenced Sep 8, 2026
os-justin
pushed a commit
that referenced
this pull request
Sep 9, 2026
… file
The merge before this one took this branch's side of an add/add conflict and
proved that drops no BYTE main carries. It does not prove it drops no CLAIM:
the header main landed makes five, and each needed a verdict against the AST
matcher that now sits underneath it. Four survived the rewrite. One had not.
RESTORED — the two regex modes' strict buckets are NESTED on this tree.
The rewrite kept "the modes are incomparable by construction" (M1) and dropped
the measurement that says the union of the two is just `path`'s bucket. M1 is
about shapes; on this tree ident-only is EMPTY, so a reader could take M1 as a
reason to run both modes and union them, which buys nothing. Re-measured on
both trees rather than copied forward:
at da5e4f6 ident 15 ⊂ path 18 ident-only EMPTY, 3 path-only
with main @ a9bc029 ident 12 ⊂ path 15 ident-only EMPTY, the SAME 3
and the same three `PermissionMatrixEditor` sites are the path-only ones in
both. The companion claim — that the mode choice was never the largest source
of movement — is restored as arithmetic the file can be re-run to check: the
mode separates THREE sites, D1 and D2 separate SIXTEEN (−7/+9, both verified
by diffing the site lists at da5e4f6).
ADDED — R2', the half of D1 that did NOT get repaired.
The window is scoped to the enclosing FUNCTION body. That kills the runaway
half (it can never reach the next `it`) and leaves the truncation half alive,
pointed the other way: a wait hosted in a helper the test awaits covers none of
the CALLER's statements. Forced on three probes — the same cross-recorder read
draws a flag with the wait inline and ZERO with it moved into a helper. Not a
corner: 503 of this tree's 3945 `await waitFor(...)` sites have their window
owned by an inner function, 464 of those a helper. The regex matcher was blind
here too, differently, so this is a surviving blind spot rather than a
regression — the header says which, and says the one shape where the old
matcher's runaway window did reach the read and this one does not.
The banner the reader running the script actually sees said the three error
sources were removed. Two were, plus half of the third; it now says that.
Also recorded: the counts move with the TREE alone. Merging main moved the
population 2776 → 2786 and every strict bucket down (AST 20 → 18, path 18 → 15,
ident 15 → 12) with the matcher byte-identical, because #8707/#8711/#8713 each
anchored a wait. That is claim 4 demonstrating itself.
Unchanged and re-verified: the census stays OUT of CI. No workflow and no
package script runs it; the only caller is the pin test, over six fixtures.
objectui#8704
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 9, 2026
… fixtures a home (objectstack-ai#8719) * test(plugin-designer): anchor the lookupKeying absence pin on the handler's reload `expect(deletes).toEqual([])` was dated to the first PUT by `waitFor(puts)`. That covers the delete scan only because the scan happens to run before the save loop in `handleObjectsChange` — a property of the page, not one the file asserts. Anchor it on `reload()`, the handler's last statement, so the emptiness is read after every write the handler makes. Measured: with a stray `reset` issued after the saves, the `puts` wait leaves this assertion green while C0/H1/H2 in the same file — which already anchor on the reload — go red. objectui#8690 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S * chore(scripts): keep objectui#8690's recorder-wait census as a script The detector answers where to look, never what is wrong, so it is a census tool and NOT wired into CI: a gate on this list would institutionalise the batch repair the card exists to prevent. Two recorder-matching modes, because the choice moves the numbers, and the header records what each one measured on the card's base commit rather than quoting the card's counts as reproduced. Changeset declares an empty frontmatter: test-only plus one repo script, nothing published moves. objectui#8690 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S * chore(scripts): record what the recorder-wait census's two matchers disagree about The census script's numbers move with an internal choice its header stated but never measured. objectui#8703 measured it, and the answer is wider than the mode choice: - The two `--recorder-match` modes are incomparable by construction — each flags a shape the other cannot see (forced on fixtures, both directions). - On da5e4f6 the buckets are nevertheless nested: ident's 15 is a subset of path's 18, ident-only is empty. "They do not contain each other" is an in-principle claim, false as a measurement of this tree. - The header's stated `path` blind spot (pushed bare, read as `host.inits[0]`) is wrong: both modes miss that shape. - Two mode-independent rules dominate the mode choice: the forward window ends at the next `await` in the FILE rather than at the end of the enclosing test, and any textual occurrence counts as a read. Seven of the eighteen strict flags on that tree point at a declaration, a destructuring, a reset or a function parameter — not at a read. Header rewritten to carry that, plus a caveat printed next to the counts. The detector's behaviour is unchanged: both modes print byte-identical site lists. Script and changeset only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S * chore(scripts): give the recorder-wait census an AST matcher, and its fixtures a home objectui#8704. The census resolved a recorder by the SPELLING of its `.push(` site, ended its forward window at the next textual `await` IN THE FILE, and counted every textual occurrence as a read. Seven of the eighteen strict flags it reported at da5e4f6 were therefore a declaration, a destructuring, a reset or a function parameter — and the same truncation went blind on a genuine cross-recorder read one ordinary `await` further on. The default matcher is now an AST pass: - identity over bindings and aliases, not names, one hop through a helper's parameter and a same-file factory's return value; - windows scoped to the enclosing test body, in statements, ending at the next awaited SETTLING anchor rather than at any `await`; - every occurrence classified read / write / declaration. The original census is kept behind `--matcher=regex`, so objectui#8690's and objectui#8703's published numbers stay reproducible from this file rather than being claims about a deleted script. Verified: at da5e4f6 it still prints 159 / 15 strict for `ident` and 167 / 18 strict for `path`, exactly as published. objectui#8703's five throwaway fixtures are committed as the script's test suite, with the sixth objectui#8704 asked for — the absence read objectui#8690 repaired, which must STAY flagged, because "flag nothing" is strictly worse than the bug and passes every other case. Both matchers are pinned over the same six files, so the repair is pinned as a direction. The header's "no count printed here is a corpus fact" caveat STAYS, with its residuals restated: no type checker, a window rule that is a judgement rather than a fact, and a flag that is still a site to read and not a defect. The census remains out of CI; the pin test runs the matcher over the fixtures only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S * chore(scripts): resolve the census header claim by claim, not file by file The merge before this one took this branch's side of an add/add conflict and proved that drops no BYTE main carries. It does not prove it drops no CLAIM: the header main landed makes five, and each needed a verdict against the AST matcher that now sits underneath it. Four survived the rewrite. One had not. RESTORED — the two regex modes' strict buckets are NESTED on this tree. The rewrite kept "the modes are incomparable by construction" (M1) and dropped the measurement that says the union of the two is just `path`'s bucket. M1 is about shapes; on this tree ident-only is EMPTY, so a reader could take M1 as a reason to run both modes and union them, which buys nothing. Re-measured on both trees rather than copied forward: at da5e4f6 ident 15 ⊂ path 18 ident-only EMPTY, 3 path-only with main @ a9bc029 ident 12 ⊂ path 15 ident-only EMPTY, the SAME 3 and the same three `PermissionMatrixEditor` sites are the path-only ones in both. The companion claim — that the mode choice was never the largest source of movement — is restored as arithmetic the file can be re-run to check: the mode separates THREE sites, D1 and D2 separate SIXTEEN (−7/+9, both verified by diffing the site lists at da5e4f6). ADDED — R2', the half of D1 that did NOT get repaired. The window is scoped to the enclosing FUNCTION body. That kills the runaway half (it can never reach the next `it`) and leaves the truncation half alive, pointed the other way: a wait hosted in a helper the test awaits covers none of the CALLER's statements. Forced on three probes — the same cross-recorder read draws a flag with the wait inline and ZERO with it moved into a helper. Not a corner: 503 of this tree's 3945 `await waitFor(...)` sites have their window owned by an inner function, 464 of those a helper. The regex matcher was blind here too, differently, so this is a surviving blind spot rather than a regression — the header says which, and says the one shape where the old matcher's runaway window did reach the read and this one does not. The banner the reader running the script actually sees said the three error sources were removed. Two were, plus half of the third; it now says that. Also recorded: the counts move with the TREE alone. Merging main moved the population 2776 → 2786 and every strict bucket down (AST 20 → 18, path 18 → 15, ident 15 → 12) with the matcher byte-identical, because objectstack-ai#8707/objectstack-ai#8711/objectstack-ai#8713 each anchored a wait. That is claim 4 demonstrating itself. Unchanged and re-verified: the census stays OUT of CI. No workflow and no package script runs it; the only caller is the pin test, over six fixtures. objectui#8704 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8665. That card asks for a component-level diagnosis of seven
contractEnvelope-6839siblings and names two as vacuous in the refusal direction. One of the two is; the other is not, and the probe says why.What was measured, not read
The family shares an idiom, and
plugin-ganttwas the one file missing itEight of the nine DOM files settle the fetch explicitly before they read:
plugin-gantthad zero occurrences. That single omission is what made it the measurably vacuous one.plugin-gantt/ObjectGantt— REPAIRED (both sinks)⭐ LEG 1, sink 2, with no bug reintroduced at all — the LIVE
dataenvelope, rows answering immediately and the quick-filter domain one macrotask later:offersP3=false offered=[p1]offered=[p1,p2,p3]offersP3=true offered=[p1,p2,p3]offered=[p1,p2,p3]The refusal assertion
expect(offersUnloadedProject(view)).toBe(false)passed against a component that resolved the whole domain. It was measuring the polling tick, not the envelope. Same result at 50ms and at 300ms.LEG 1, sink 1, uniform delay: the DOM at the old wait was the⚠️ Reported honestly as a spurious red, not a silent pass — I could not construct a state where sink 1 passes vacuously, because
Loading Gantt chart...placeholder and the read threwUnable to find an element by: [data-testid="gantt-view"].gantt-viewrenders only below theif (loading)early return.LEG 2 (new wait under the same forced ordering) and LEG 3 (ordinary ordering): every arm correct at 0ms / 50ms / 300ms, rows-late and domain-late alike.
Anchors chosen from the component's own mechanism
gantt-view.loadingis cleared inreload'sfinally, in the sameisCurrent()-guarded commit assetData, so its presence proves the row query settled AND committed.ObjectGanttpublishes no DOM marker for the domain fetch on the refusing branch — a refused domain leaveslookupOptions[field]empty, the resolver falls through to "distinct values present in the loaded data", and the dropdown looks exactly as it does before the fetch answers. So the fixture adds a second lookup (owner) resolved by the same effect:setLookupOptionsis called once, after the loop over every dimension, and no loaded task carries anowner, so its option can only come from the domain fetch having been read and committed.Both anchors are PRESENCE assertions, so neither arm can pass by timing out.
Would an implementation strictly worse than the bug pass? Ablating
extractRecordsto "return nothing, ever":Unable to find an element by: [data-testid="quick-filter-option-owner-u1"])sink 1's refusal arm still reads 0 under that ablation; its positive arms refuse it. That is the whole family's documented design — all nine behave this way, the already-repaired
plugin-treeincluded — so it is stated rather than claimed fixed.plugin-dashboard/ObjectPivotTable— NOT repaired, premise falsifiedThe card says the wait
queryByTestId('pivot')cannot separate "drawn empty" from "drawn before the data arrived". Measured, it can — and the anchor is a line the card's reading skipped, two lines above the wait it quotes.With the
recordsarm reintroduced ahead ofdata(the pre-fix bug), the refusal arm reddens at 0ms, 50ms and 300ms. It still reddens withObjectPivotTable's loading-skeleton early return also disabled. Removing only the settle line, it goes vacuous exactly as predicted:⇒ the file already carries its completion anchor. Left alone deliberately.
The other five — diagnosed, not touched
All five carry the settle-line idiom, and in every one the waited-for node sits below a
loadingearly return, so the wait is gated twice.plugin-calendar/ObjectCalendarloadingstartstrue;if (loading)returns a placeholder, so the stubbed grid cannot mount early. Its comment calls the testid a mount signal — true and deliberate: it is envelope-INDEPENDENT, which is what lets one wait serve every arm.loadingand by the settle line.plugin-charts/ObjectChartReact.lazylives inChartRenderer.tsx— which this pinvi.mocks away, so the pin never crosses it. The refusal arm asserts'empty-state', a DIFFERENT node from the renderer, gated byif (loading && finalData.length === 0)..not.toBe('Apollo'), whichundefinedsatisfies — the objectui#8688 shape. Filed separately.plugin-dashboard/ObjectDataTable'empty-state'— a distinct node, not a count of 0.if (loading && finalData.length === 0)gates both.plugin-map/ObjectMaploadingstartstrue;setLoading(false)commits withsetData. Correct at 0/50/300ms with and without the settle line.Loading map...panel is never observed at all, so the wait is satisfied with no transition having occurred. It holds only because those two setStates commit together, and nothing pins that.plugin-timeline/ObjectTimelineif (loading && (!effectiveItems || effectiveItems.length === 0))gates the renderer.getByTestIdthrowing insidewaitForis what gates it; thedata-item-count.not.toBeNull()half is inert, satisfied by'0'.⛔ No component was changed. Two component-layer findings are reported to the card, not fixed here.
Verification
pnpm exec vitest run packages/plugin-gantt/— 64 files, 502 tests, all passed.tsc -p packages/plugin-gantt/tsconfig.test.jsonwith the dependency closure built (pnpm --filter '@object-ui/plugin-gantt^...' build) — exit 0, TS2307 count 0 so the precondition was met, and--listFilesconfirms the changed file is in the program.eslint .in@object-ui/plugin-gantt— 92 files, 0 errors, exit 0. Warnings 362 (package-wide, pre-existing). The changed file went 6 → 9@typescript-eslint/no-explicit-anywarnings: three new helpers typedview: any, matching the convention every helper in that file already uses.check:vi-mock-specifiers,check:vi-mock-inherit,check:control-bytes,check-changeset-presence,check-changeset-no-major— all exit 0.extractRecords), never a pin, from a committed tree, withtrapon EXIT/INT/TERM, absolute paths, on-disk proof in both directions (anchor counts ANDgit hash-objectvs the HEAD blob) plus a line-total gate, and restore proven by state (git diff HEADempty).plugin-mapprobe omitted the pin'sreact-map-gl/maplibremock and measured nothing, and a first pass leaked containers across runs. Both were rebuilt before any conclusion was drawn.Changeset: empty frontmatter — test-only, releases nothing.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code