Repository navigation
Conversation
added 4 commits
September 9, 2026 20:47
Reordering packs on the device is the only way to change the order the Lunii plays them in. Nothing exercised that path above the pack index writer, at either end of it. Two levels, both characterization: PackIndexReorderValidationTest pins what FsStoryTellerAsyncDriver.reorderPacks accepts. Its guard checks that every submitted UUID is on the device but not the converse, so a partial list is honoured and every pack the caller left out sorts by indexOf == -1 and lands at the front of the device. Duplicates pass, and an empty list still rewrites the index because allMatch over an empty stream is vacuously true. That is reachable from the UI rather than hypothetical: getPacksList() drops index entries whose .content folder has no ni file, so the list the browser sends back can legitimately be shorter than the index it reorders. The assertions record the defect, they do not bless it. packLibraryReorder.test.js drives the drag & drop handlers through React's own event system, which needed the component exported. No rendering library is added: react-dom ships test-utils, and Simulate propagates through the tree, so a dragleave simulated on a tile reaches the dropzone handler as a bubbling one would.
The device list is a scrolling box showing a few tiles at a time -- at common window widths its grid is a single column, so a dozen packs stack well past the fold. A drag raises dragenter only for tiles the pointer actually reaches, and the browser's own drag scrolling does not drive an inner scroll container, so a pack could only ever be dropped beside a tile that was already on screen. Reordering into anything below the fold was simply not expressible. Driven by a frame loop rather than by dragover alone: dragover stops firing while the pointer is held still, which is exactly what someone does when waiting for a list to scroll. The loop is cancelled on drop, on dragend and on unmount, so it cannot outlive the drag that started it.
The scrolling itself shipped in the previous commit with nothing exercising it. These are specifications rather than characterization: the behaviour is new, and what it promises -- a pack below the fold becomes a reachable drop target -- is only true if the loop keeps running while the pointer is held still, and only safe if it stops on drop, on dragend and on unmount. The frame loop is driven by hand. jsdom has no layout, so a real requestAnimationFrame would only make the test wait; and scrollTop stays 0 whatever the component assigns to it, so the zone is backed by a plain property. What is asserted is what the component writes on each frame, which is the contract that matters here.
Dev mode is the only way to exercise the web UI without a Lunii plugged in, and reorderPacks on the mocked device was a `// Not supported` stub returning false. DeviceController turns that false into a 500, so a drag and drop could be driven all the way through the browser and was then told, every time, that the reorganisation had failed. The one screen that cannot be reasoned about from tests alone -- drag, drop, scroll -- was the one dev mode could not show working. The mock keeps one file per pack and a folder listing carries no order, so the order has to be stored. It goes in a file beside the packs, which is where a real device keeps its own pack index too. Not a pack file, so the listing skips it without being taught to. Two behaviours are copied from FsStoryTellerAsyncDriver rather than improved on: a partial list is honoured, with every pack the caller left out sorting by indexOf == -1 and landing at the front, and a list naming a pack the device does not hold is refused outright. That is what PackIndexReorderValidationTest pins at the other end. A mock that quietly behaved better would hide the real device's surprises in the only mode that can be run without hardware.
lgnap
force-pushed
the
diagnose/device-reorder
branch
from
September 9, 2026 18:47
54d001f to
8d6298d
Compare
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.
Reordering packs on the device is the only way to change the order the Lunii plays them in. Three things were in the way of it: the list could not be scrolled during a drag, nothing tested the path at either end, and dev mode refused the operation outright.
The bug
The device list is a scrolling box showing a few tiles at a time — at common window widths its grid is a single column, so a dozen packs stack well past the fold. A drag raises
dragenteronly for tiles the pointer actually reaches, and the browser's own drag scrolling does not drive an inner scroll container. A pack could therefore only ever be dropped beside a tile that was already on screen: reordering into anything below the fold was simply not expressible.The fix drives the scroll from a frame loop rather than from
dragoveralone, becausedragoverstops firing while the pointer is held still — which is exactly what someone does when waiting for a list to scroll. The loop is cancelled on drop, ondragendand on unmount, so it cannot outlive the drag that started it.Tests
PackIndexReorderValidationTestpins whatFsStoryTellerAsyncDriver.reorderPacksaccepts. Its guard checks that every submitted UUID is on the device but not the converse, so a partial list is honoured and every pack the caller left out sorts byindexOf == -1and lands at the front. That is reachable from the UI rather than hypothetical:getPacksList()drops index entries whose.contentfolder has nonifile, so the list the browser sends back can legitimately be shorter than the index it reorders. The assertions record the defect, they do not bless it.packLibraryReorder.test.jsdrives the drag and drop handlers through React's own event system.packLibraryAutoScroll.test.jsspecifies the new scrolling: the frame loop is driven by hand, since jsdom has no layout and a realrequestAnimationFramewould only make the test wait.Dev mode
MockStoryTellerService.reorderPackswas a// Not supportedstub returningfalse, whichDeviceControllerturns into a 500. The whole drag and drop could be driven in the browser and was then told, every time, that the reorganisation had failed — so the one screen that cannot be reasoned about from tests alone was the one screen dev mode could not show working. The mock now stores the submitted order in a file beside the packs, which is where a real device keeps its own pack index too, and copies the real driver's two surprises rather than improving on them: a partial list is honoured, and a list naming an absent pack is refused.Verification
mvn test)🤖 Generated with Claude Code