Unify the trashed universe on the trash rows
02 rules the trashed side has exactly one definition: the set with trash rows — a card carrying its own deleted: under a tombstoned lane is in neither universe, so an anchor or selection can never survive on an item that renders nowhere. The membership rule now lives once, in Liveness.walk, and ItemReferenceSet.idUniverse, TrashModel.entries, paths, and emptyTrashTargets all derive from it — the old lane-OR-card logic that admitted subsumed cards to the trashed side is gone, and the two universes deliberately no longer partition the board. Put Back, Delete Immediately, and Empty Trash outcomes are unchanged: a tombstoned lane still moves and purges whole, its nested tombstones with it. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -137,6 +137,56 @@ struct TransientBoardStateTests {
|
||||
#expect(store.transient.pendingCut.ids == [card3], "card3's lane is untouched, so card3 stays cut")
|
||||
}
|
||||
|
||||
@Test("A card with its own deleted: under a tombstoned lane is in neither universe")
|
||||
func ownFlaggedCardUnderATombstonedLaneIsInNeitherUniverse() async throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
|
||||
// Everything that can point at an item, all aimed at card3 — and on the *trashed* side, as
|
||||
// if the user had clicked its trash row a moment before its lane went too.
|
||||
store.transient.select([card3], liveness: .trashed, anchor: card3, head: card3)
|
||||
store.transient.dragMembers = ItemReferenceSet(ids: [card3], liveness: .trashed)
|
||||
store.transient.pendingCut = ItemReferenceSet(ids: [card3], liveness: .trashed)
|
||||
store.transient.beginRename(of: card3, currentTitle: "Third")
|
||||
|
||||
// Both flags at once: the card carries its own `deleted:` *and* an agent tombstones its lane.
|
||||
try fixture.item("\(Ident.lane2)/\(Ident.card3)", tombstoned(order: "1024", title: "Third"))
|
||||
try fixture.item(Ident.lane2, tombstoned(order: "2048", title: "Doing"))
|
||||
await reload(store)
|
||||
|
||||
let lane = try #require(store.snapshot.lanes.first { $0.id == lane2 })
|
||||
#expect(lane.isDeleted)
|
||||
#expect(lane.cards.first { $0.id == card3 }?.isDeleted == true, "the card's own flag is on disk")
|
||||
|
||||
// The lane's single entry subsumes it (03-board-ui.md's absolute ancestor walk), so it has
|
||||
// no row — and "the trashed side has exactly one definition: the set with trash rows"
|
||||
// (02-architecture.md, settled). No row, no membership, on either side.
|
||||
#expect(!TrashModel.entries(of: store.snapshot).map(\.id).contains(card3))
|
||||
#expect(!ItemReferenceSet.idUniverse(of: store.snapshot, on: .trashed).contains(card3))
|
||||
#expect(!ItemReferenceSet.idUniverse(of: store.snapshot, on: .live).contains(card3))
|
||||
|
||||
// So every set holding it is ejected — from the trashed side here, and from the live side
|
||||
// for the same reason, which the value function says directly since a set has one side.
|
||||
#expect(store.transient.selection.ids.isEmpty)
|
||||
#expect(store.transient.dragMembers.ids.isEmpty)
|
||||
#expect(store.transient.pendingCut.ids.isEmpty)
|
||||
#expect(ItemReferenceSet(ids: [card3], liveness: .live).resolved(against: store.snapshot).isEmpty)
|
||||
|
||||
// And no cursor or editor survives on it: an anchor that ranges from somewhere the board
|
||||
// draws nowhere would be a range the user cannot see the origin of.
|
||||
#expect(store.transient.selectionAnchor == nil)
|
||||
#expect(store.transient.selectionHead == nil)
|
||||
#expect(store.transient.renameEditor == nil)
|
||||
|
||||
// Menu validation agrees, which is the point of the sets and the commands reading one rule:
|
||||
// neither ⌘⌫ twin offers to act on it.
|
||||
let stale = ItemReferenceSet(ids: [card3], liveness: .trashed)
|
||||
#expect(!TrashModel.canActOnTrash(selection: stale, in: store.snapshot))
|
||||
#expect(!TrashModel.canDelete(selection: ItemReferenceSet(ids: [card3], liveness: .live),
|
||||
in: store.snapshot))
|
||||
}
|
||||
|
||||
@Test("Every set resolves to empty against a board whose lanes all vanished")
|
||||
func everythingResolvesToNothingOnAnEmptyBoard() async throws {
|
||||
let fixture = try makeBoard()
|
||||
|
||||
Reference in New Issue
Block a user