test(plugin-kanban): characterize the cross-column landing position (objectui#8826 Q3) - #8975
Merged
os-steve merged 1 commit intoSep 10, 2026
Conversation
…objectui#8826 Q3)
Answers the third, explicitly unmeasured question on the card: on a
cross-column move, is the landing POSITION within the target column also
lost? It is, on both data ownerships.
`KanbanImpl.handleDragEnd` computes a landing index, splices the card in at
it, and hands the index to `onCardMove`. `ObjectKanban.handleCardMove`
discards it (`void _newIndex`) and `persistCardMove` writes a body of exactly
the groupBy column. The index reaches a parameter and stops there.
The two ownerships differ in WHEN the loss becomes visible, which the card did
not anticipate:
- internal data: the optimistic write re-renders the board, which re-buckets
and re-syncs the mirror from the prop, so the card leaves the dropped slot
immediately -- a visible snap, not a later silent revert.
- external data: the optimistic write is skipped, nothing re-renders, and the
dropped slot survives on screen until the parent's next reflow.
Characterization only -- zero behaviour change. The recorder captures the
`columns` prop on every board render, driven through the real `onDragEnd`, and
carries both a lit positive control (the drop does produce a new snapshot,
with the membership change in it) and a dead negative control (a drag that
ends on nothing records neither snapshot nor write).
The changeset declares an empty frontmatter: the package publishes `dist` only,
so a file under `src/__tests__/` releases nothing.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
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-steve
marked this pull request as ready for review
September 10, 2026 13:32
os-steve
deleted the
claude/issue-8826-crosscolumn-landing-position-probe
branch
September 10, 2026 13:50
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.
Refs #8826 — answers that card's question 3 only. The card stays OPEN.
What this is
objectui#8826 measured that a SAME-column reorder is persisted nowhere. Its third question — on a CROSS-column move, is the landing POSITION within the target column also lost? — was filed explicitly unmeasured, with the instruction to answer it before touching anything, because the fix surface depends on the answer.
This PR is that measurement, frozen as a characterization test.
Zero behaviour change. No persistence added. No optimistic update removed.
KanbanImpl's mirror-sync timing untouched — that belongs to objectui#8534 / PR #8825 and has already landed. The card's questions 1 and 2 are deliberately not pre-empted; they stay with the maintainer.The answer: the position is lost too — on BOTH data ownerships
KanbanImpl.handleDragEnd's cross-column branch does compute a landing index, does splice the card in at it, and does hand that index toonCardMoveas its fourth argument.ObjectKanban.handleCardMovereceives it as_newIndexand discards it outright (void _newIndex);persistCardMovewrites a body of exactly thegroupBycolumn. The index reaches a parameter and stops there — no sink, no write key, no storage.⭐ The part the card did not anticipate: the two ownerships answer differently
columnssnapshot after the dropbacklog: OR in_progress:Xray,Yankee,Alpha— record orderact()data, e.g. ListView)The optimistic write that re-renders the board is itself gated on ownership. On the internal path it fires,
KanbanRendererre-buckets, thecolumnsidentity changes and the mirror re-syncs — so the loss is a visible snap on the spot, which is worse than the delayed silent revert the card describes for the same-column case. On the external path the optimistic write is skipped, nothing re-renders, and the local move stands until the parent reflows — that is the card's silent-revert signature exactly.⇒ Position is lost on both. They differ only in when the user sees it happen.
(The table's column-order cells are written with the word
ORwhere a vertical bar would be, because a bar is the markdown table cell separator. The test's own constants use the bar.)The readings
Fixture is built so the two orders cannot coincide: the moved card is last in the record array and is dropped first in the target column. A fixture where the dropped card also happened to sort first would read green either way.
Records:
Xray(in_progress),Yankee(in_progress),Alpha(backlog). DropAlphaontoXray= slot 0 of "In Progress".backlog:Alpha/in_progress:Xray,Yankeein_progress:Alpha,Xray,Yankee→ never recorded, on either pathin_progress:Xray,Yankee,Alpha; DOM['Xray','Yankee','Alpha']['Alpha','Xray','Yankee']update('task', 'a', { status: 'in_progress' }), exact body, no positional keyLit positive control (the card's own): the cross-column drop does produce a new
columnssnapshot carrying the membership change ⇒ the instrument is connected, so "the drop order never appears" is a reading and not a dead wire.Dead negative control: a drag that ends on nothing records 0 snapshots and makes 0 writes ⇒ the instrument does not fire spuriously.
Ablation — proving the pin discriminates
A characterization pin is green on
mainby construction, so its value is that it goes RED when the behaviour moves. Mutation applied at the discard anchor inObjectKanban.tsx: honour the index by hoisting the record to the front of the source array when the card was dropped into slot 0. Deliberately crude — not a candidate fix, only enough of a behaviour change to prove the pin can see one.Run against
513fe387d.ObjectKanban.tsx2a6d42aac9aca111797d4ab4386cf28df46f50ed33c2b6f2217c407b7565691c7140ad1bff33a2c12a6d42aac9aca111797d4ab4386cf28df46f50edRestore proven two ways, not by an exit code: the on-disk blob hash reproduces the HEAD blob byte for byte, and
git diff HEADis empty. The mutation was proven to reach the disk before the RED arm ran (anchor counts and a changed blob hash), so a no-op edit could not have been mistaken for a passing run.The two failures are exactly the two measurement assertions; the two controls, the external-path asymmetry and the wire assertion stay green — the mutation only moves internal-path ordering. That failure profile, not just the red, is what says the pin is aimed at the right thing.
Gates
pnpm --filter @object-ui/plugin-kanban type-check(both legs:tsc --noEmit+tsconfig.test.json)pnpm --filter @object-ui/plugin-kanban lintpnpm exec vitest run packages/plugin-kanban/(from repo ROOT)check-control-bytes.mjscheck-changeset-presence.mjscheck-governed-queue-guard.mjs --test(both paths)check:doc-typescheck:doc-snippetscheck:doc-examplescheck:vi-mock-specifiers/-inherit/-override-shapeThe
vi-mocktrio is not from the dispatched list — this diff adds twovi.mockcall sites, and those three gates read exactly that, so they were run as implicated.The doc gates first exited 2 (
PREREQUISITE NOT MET, packages unbuilt); the scoped build the gate names was run and both were re-run to a real 0. Exit 2 was not read as a pass.Changeset carries an empty frontmatter: the package publishes
distonly, so a file undersrc/__tests__/releases nothing. That is the gate's own documented pass for a test-only change, not a workaround.Acceptance notes
scripts/check-doc-example-types.mjshas no CI job ("0 hits across all of.github/andturbo.json"). It has one:.github/workflows/doc-snippet-types.ymlrunsnode scripts/check-doc-example-types.mjsas a real job step, onpull_request,push,merge_groupandworkflow_dispatch. The gate's own workflow filename does not carry its name, which is the likely reason a filename-shaped search missed it; that same workflow's header even documents the gate as a sibling. Consequence: the "nothing in CI will tell you" warning attached to theUNGATED_EXAMPLESledger is wrong — CI does tell you. Noted, not filed: this is a correction to a dispatch brief, not a repo defect, and no PR or file carries it. Successor: none.ObjectKanban.tsx:1081-1083; on today'smainit is at:1224-1226. Re-derived by anchor, as instructed. The mechanism is unchanged:_newIndexis a declared parameter,void _newIndexis the discard, andhandleCardMoveearly-returns onfromColumnId === toColumnId. Noted, not filed — line numbers in an issue body are not a defect.packages/plugin-kanban/at all, PR chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941, and only itspackage.json(a dependency bump). No open PR touches this package's source or tests. No overlap.columnsReachDomSameCommit-8534.test.tsxsnapshots the DOM per commit at theKanbanImplseam, a different instrument, and it never drives a cross-column drop. Thecolumns-prop recorder was therefore built here as a module mock, matching the card's description so the readings are directly comparable.What the maintainer now needs in order to rule Q1 and Q2
The measurement changes the shape of question 2. The card framed today's behaviour as "let the user think it worked, then silently undo it" and offered a snap-back as the honest-but-dumber alternative. On the internal-data path the snap-back already happens — immediately, unprompted, with no animation or message. So:
KanbanImplalready computes and forwards it. Persisting in-column order would need a sink and a storage contract, but it would not need new plumbing to learn where the card was dropped.🤖 Generated with Claude Code
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Generated by Claude Code