Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires (Ranks.isOrderedForTrash one comparator, loader + merged order agree; the legacy deleted: migration stamps modified from the tombstone timestamp where parseable; delete undo steps validate existence-only; agent guide v8). Trash selection goes kind-blind — ranges, marquee, Select All, and the successor walk sweep both kinds; the guard moves to the exits (mixed-payload drop refusal, copy/cut validation). The copy stamping preflight widens back to comment depth (load-scoped posture — the board always loads, the gesture refuses whole). Fixes a latent no-op: trashed-lane drag restore never fired (DragSession.beginLanes hard-coded the board container). 2403 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -669,6 +669,55 @@ struct ClipboardAvailabilityTests {
|
||||
== ItemReferenceSet(ids: [clipboardTrashedLane], container: .trash))
|
||||
}
|
||||
|
||||
/// **The guard the trash's kind-blind selection moved to the exits** (04-interactions.md ▸ The
|
||||
/// trash, ruled 2026-07-31): "the pasteboard's payload types are per-kind, so Cut and Copy grey
|
||||
/// out via ordinary menu validation while a trash selection mixes kinds — no failed gesture, no
|
||||
/// beep".
|
||||
///
|
||||
/// It is also what keeps `ClipboardManifest.kind` honest: the manifest names one payload type,
|
||||
/// and a set spanning both never reaches the capture.
|
||||
@Test("A mixed trash selection greys out both Copy and Cut")
|
||||
func mixedTrashSelectionClosesCopyAndCut() throws {
|
||||
let harness = try makeTrashedLaneHarness()
|
||||
defer { harness.tearDown() }
|
||||
harness.store.transient.isTrashVisible = true
|
||||
|
||||
// Each kind alone is fine — the selection is legal either way, and so is the gesture.
|
||||
harness.store.select([clipboardCard3], in: .trash)
|
||||
#expect(harness.clipboard.canCopy(from: harness.store))
|
||||
harness.store.select([clipboardTrashedLane], in: .trash)
|
||||
#expect(harness.clipboard.canCopy(from: harness.store))
|
||||
|
||||
// Together — a selection the grammar now allows — the exits close.
|
||||
harness.store.select([clipboardCard3, clipboardTrashedLane], in: .trash)
|
||||
#expect(harness.store.selection.ids.count == 2, "the selection itself is legal")
|
||||
#expect(harness.clipboard.canCopy(from: harness.store) == false)
|
||||
#expect(harness.clipboard.canCut(from: harness.store) == false)
|
||||
// Delete is deliberately *not* gated: it works on a mixed selection, the alert counting both
|
||||
// kinds (04 ▸ The trash).
|
||||
#expect(TrashModel.canDelete(selection: harness.store.selection, in: harness.store.snapshot))
|
||||
}
|
||||
|
||||
/// The live board's own mixed set cannot be built by any gesture — but the predicate answers for
|
||||
/// it anyway rather than assuming, so a future caller cannot smuggle one past the exits.
|
||||
@Test("The mixed-kind predicate answers for the live board too")
|
||||
func mixedKindPredicateCoversTheBoard() throws {
|
||||
let harness = try makeClipboardHarness()
|
||||
defer { harness.tearDown() }
|
||||
let snapshot = harness.store.snapshot
|
||||
let laneID = try #require(snapshot.lanes.first?.id)
|
||||
|
||||
#expect(!SelectionGrammar.mixesKinds(
|
||||
ItemReferenceSet(ids: [clipboardCard1], container: .board), in: snapshot))
|
||||
#expect(SelectionGrammar.mixesKinds(
|
||||
ItemReferenceSet(ids: [clipboardCard1, laneID], container: .board), in: snapshot))
|
||||
// Rows the container no longer holds are ignored — a ghost must not grey out a menu item.
|
||||
#expect(!SelectionGrammar.mixesKinds(
|
||||
ItemReferenceSet(ids: [clipboardCard1, ItemID(rawValue: Ident.indexless)], container: .board),
|
||||
in: snapshot))
|
||||
#expect(!SelectionGrammar.mixesKinds(.empty, in: snapshot))
|
||||
}
|
||||
|
||||
@Test("The read-only lock blocks cut but never copy")
|
||||
func lockBlocksCutOnly() throws {
|
||||
let harness = try makeClipboardHarness()
|
||||
|
||||
Reference in New Issue
Block a user