Realign code with the 2026-07-30 findings-resolution rulings
Delete Immediately is removed entirely (rulingae1dd96, Redesign card d40bfac1): the delete vocabulary is purely staged — board → trash, trash → permanent (confirmed on no-git boards), Empty Trash for bulk. Gone: File ▸ Delete Immediately (⌥⌘⌫) and its validation, both ⌥-alternate context rows (card + the permanently-disabled lane row), the VO custom action, BoardStore.deleteImmediately, TrashModel.canDeleteImmediately, TrashConfirmations' .purge action (zero surviving callers — trash-side Delete always used .deleteTrashCards), and the pinning tests. purgePrompt drops its now-single-purpose container parameter (.trash is the only surviving caller). BoardWriter.purgeItem survives — create-undo rollback still needs it — with its comment rewritten. README's trash paragraph drops the ⌥⌘⌫ sentence. The other 2026-07-30 rulings (2ec2c95registry freshness stamp,c741b02unified-log-as-coerce-consumer) required no code changes — already conformant. Both schemes 1844 tests / 318 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -2,9 +2,8 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Kanban
|
||||
|
||||
/// `BoardStore`'s trash operations — Delete (staged by place), Delete Immediately, Empty Trash, and
|
||||
/// the legacy tombstone migration (03-board-ui.md § Trash, resettled 2026-07-28; 01-storage-format.md
|
||||
/// § Deletion).
|
||||
/// `BoardStore`'s trash operations — Delete (staged by place), Empty Trash, and the legacy tombstone
|
||||
/// migration (03-board-ui.md § Trash, resettled 2026-07-28; 01-storage-format.md § Deletion).
|
||||
///
|
||||
/// These drive a **real store over a real temp board** and then read the **raw bytes** back, never
|
||||
/// the app's own read path, like every other write suite here: the interesting claims are about the
|
||||
@@ -377,51 +376,12 @@ struct StagedDeleteTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete Immediately and Empty Trash
|
||||
// MARK: - Empty Trash
|
||||
|
||||
@MainActor
|
||||
@Suite("BoardStore ▸ purge")
|
||||
struct PurgeTests {
|
||||
|
||||
@Test("Delete Immediately skips the trash from a lane")
|
||||
func skipsTheTrashFromTheBoard() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.select([card1], in: .board)
|
||||
|
||||
store.deleteImmediately([card1], in: .board)
|
||||
|
||||
#expect(!fixture.exists("\(Ident.lane1)/\(Ident.card1)"))
|
||||
#expect(!fixture.exists(".trash/\(Ident.card1)"), "03 ▸ Trash: ⌥⌘⌫ skips the trash from anywhere")
|
||||
#expect(store.selection.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Delete Immediately purges a card already in the trash")
|
||||
func purgesFromTheTrash() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.select([trashed], in: .trash)
|
||||
|
||||
store.deleteImmediately([trashed], in: .trash)
|
||||
|
||||
#expect(!fixture.exists(".trash/\(Ident.indexless)"))
|
||||
#expect(fixture.exists(".trash/\(More.newer)"), "and only what it named")
|
||||
}
|
||||
|
||||
@Test("A lane in the set is never purged — cards only")
|
||||
func lanesAreNotPurged() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.select([lane3], in: .board)
|
||||
|
||||
store.deleteImmediately([lane3], in: .board)
|
||||
|
||||
#expect(fixture.exists(Ident.lane3))
|
||||
}
|
||||
|
||||
@Test("Empty Trash removes every card in the container, and leaves strays verbatim")
|
||||
func emptyTrashIsWholeScope() throws {
|
||||
let fixture = try makeBoard()
|
||||
@@ -473,10 +433,10 @@ struct PurgeTests {
|
||||
|
||||
store.select([trashed], in: .trash)
|
||||
store.deleteTrashCards([trashed])
|
||||
store.deleteImmediately([newer], in: .trash)
|
||||
store.deleteTrashCards([newer])
|
||||
store.emptyTrash()
|
||||
|
||||
// 13-native-undo.md ▸ Rules: "Permanently delete (Delete Immediately, Empty Trash) …
|
||||
// 13-native-undo.md ▸ Rules: "Permanently delete (the trash's Delete, Empty Trash) …
|
||||
// the confirm *is* the safety." A stack entry here would be a promise the filesystem
|
||||
// cannot keep.
|
||||
#expect(!history.canUndo)
|
||||
@@ -733,95 +693,6 @@ struct MigrationNoticeTests {
|
||||
@Suite("TrashConfirmations")
|
||||
struct TrashConfirmationsTests {
|
||||
|
||||
@Test("Delete Immediately raises the alert where the loss is real, and purges on confirm")
|
||||
func purgeConfirmsThenActs() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let confirmations = TrashConfirmations()
|
||||
store.select([trashed], in: .trash)
|
||||
|
||||
confirmations.requestPurge(of: [trashed], in: store)
|
||||
|
||||
let pending = try #require(confirmations.pending)
|
||||
#expect(pending.prompt.title == "Permanently delete \u{201C}Trashed\u{201D}?")
|
||||
#expect(pending.action == .purge([trashed], .trash))
|
||||
// Nothing has happened yet — the alert is what stands between the keystroke and the loss.
|
||||
#expect(fixture.exists(".trash/\(Ident.indexless)"))
|
||||
|
||||
confirmations.confirm(in: store)
|
||||
#expect(!fixture.exists(".trash/\(Ident.indexless)"))
|
||||
#expect(confirmations.pending == nil)
|
||||
// Idempotent: the binding's own dismissal fires an instant after the button.
|
||||
confirmations.confirm(in: store)
|
||||
}
|
||||
|
||||
/// **The card and lane context menus' ⌥-alternate** — Delete Immediately, routed through
|
||||
/// `requestBoardPurge` rather than through `requestPurge` (11-command-nexus.md ▸ Context menus'
|
||||
/// Card and Lane rows: "Delete — with Delete Immediately as its ⌥-alternate").
|
||||
/// `purgeConfirmsThenActs`'s twin for the board side: same alert, same rule, a board card as the
|
||||
/// target instead of a trash one.
|
||||
@Test("The board-side ⌥-alternate raises the same alert, and purges the board card on confirm")
|
||||
func boardPurgeConfirmsThenActs() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let confirmations = TrashConfirmations()
|
||||
|
||||
confirmations.requestBoardPurge(of: [card1], in: store)
|
||||
|
||||
let pending = try #require(confirmations.pending)
|
||||
#expect(pending.prompt.title == "Permanently delete \u{201C}First\u{201D}?")
|
||||
#expect(pending.action == .purge([card1], .board))
|
||||
// Nothing has happened yet — same alert, same rule.
|
||||
#expect(fixture.exists("\(Ident.lane1)/\(Ident.card1)"))
|
||||
|
||||
confirmations.confirm(in: store)
|
||||
#expect(!fixture.exists("\(Ident.lane1)/\(Ident.card1)"))
|
||||
#expect(!fixture.exists(".trash/\(Ident.card1)"), "skips the trash — purged, not moved")
|
||||
#expect(confirmations.pending == nil)
|
||||
}
|
||||
|
||||
/// A context menu names its target by where it was invoked, so a card row's Delete Immediately
|
||||
/// must purge the *clicked* card even while a different card is selected — `TrashMenuValidation
|
||||
/// Tests.contextMenuDeleteIgnoresTheSelection`'s claim, mirrored onto the board side.
|
||||
@Test("The board-side ⌥-alternate acts on its own target, not the standing selection")
|
||||
func boardPurgeIgnoresTheSelection() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let confirmations = TrashConfirmations()
|
||||
// A right-click on `card1` without first selecting it must still purge `card1`, never the
|
||||
// card the standing selection happens to hold (`CardFaceView.targetIDs`'s targeting rule).
|
||||
store.select([card2], in: .board)
|
||||
|
||||
confirmations.requestBoardPurge(of: [card1], in: store)
|
||||
let pending = try #require(confirmations.pending)
|
||||
#expect(pending.action == .purge([card1], .board))
|
||||
|
||||
confirmations.confirm(in: store)
|
||||
#expect(!fixture.exists("\(Ident.lane1)/\(Ident.card1)"), "the clicked card is gone")
|
||||
#expect(fixture.exists("\(Ident.lane1)/\(Ident.card2)"), "the selected card was never the subject")
|
||||
}
|
||||
|
||||
/// `TrashModel.canDeleteImmediately` is cards only (`TrashValidationTests
|
||||
/// .canDeleteImmediatelyIsCardsOnly`: "a lane's delete is physical already … there is nothing for
|
||||
/// 'skip the trash' to mean on one"), and the lane row's alternate inherits that unchanged: it is
|
||||
/// wired per 11-command-nexus.md's Lane row, but presently inert on a lane-only target — the same
|
||||
/// posture File ▸ Delete Immediately already takes on a lane-only selection.
|
||||
@Test("A lane-only target raises no prompt — the alternate is still cards only")
|
||||
func boardPurgeIsStillCardsOnlyForALaneTarget() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let confirmations = TrashConfirmations()
|
||||
|
||||
confirmations.requestBoardPurge(of: [lane1], in: store)
|
||||
|
||||
#expect(confirmations.pending == nil)
|
||||
#expect(fixture.exists(Ident.lane1))
|
||||
}
|
||||
|
||||
/// 03-board-ui.md § Trash: "on a trash card, Delete (⌫/⌘⌫) is permanent … Both confirm exactly
|
||||
/// where the loss is real."
|
||||
@Test("The trash's own Delete confirms; the board's goes straight through")
|
||||
@@ -856,7 +727,7 @@ struct TrashConfirmationsTests {
|
||||
let confirmations = TrashConfirmations()
|
||||
store.select([trashed], in: .trash)
|
||||
|
||||
confirmations.requestPurge(of: [trashed], in: store)
|
||||
confirmations.requestTrashDelete(of: [trashed], in: store)
|
||||
confirmations.cancel()
|
||||
|
||||
#expect(confirmations.pending == nil)
|
||||
@@ -892,20 +763,21 @@ struct TrashConfirmationsTests {
|
||||
confirmations.requestEmptyTrash(in: store)
|
||||
#expect(confirmations.pending == nil)
|
||||
|
||||
confirmations.requestPurge(of: [lane1], in: store)
|
||||
// A board lane names nothing in the trash — the trash-side Delete's own refusal.
|
||||
confirmations.requestTrashDelete(of: [lane1], in: store)
|
||||
#expect(confirmations.pending == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The menu-validation seams
|
||||
|
||||
/// The trash's three File-menu rows, validated as predicates rather than as menu items — 11-command
|
||||
/// The trash's two File-menu rows, validated as predicates rather than as menu items — 11-command
|
||||
/// -nexus.md's inventory, and 03-board-ui.md § Trash's rulings about scope.
|
||||
///
|
||||
/// The rows themselves are `TrashCommands`, whose whole body is one `disabled(…)` per row over these
|
||||
/// answers; what is worth pinning is the answers. `TrashModel.canDelete`/`canDeleteImmediately` are
|
||||
/// pinned as pure functions in `TrashModelTests`; this suite covers the two seams that need a live
|
||||
/// store — Empty Trash's scope, and the staging a ⌘⌫ actually performs.
|
||||
/// answers; what is worth pinning is the answers. `TrashModel.canDelete` is pinned as a pure function
|
||||
/// in `TrashModelTests`; this suite covers the two seams that need a live store — Empty Trash's scope,
|
||||
/// and the staging a ⌘⌫ actually performs.
|
||||
@MainActor
|
||||
@Suite("The trash's menu validation")
|
||||
struct TrashMenuValidationTests {
|
||||
@@ -1076,7 +948,6 @@ struct TrashGrammarExclusionTests {
|
||||
store.select([trashed], in: .trash)
|
||||
|
||||
#expect(TrashModel.canDelete(selection: store.selection, in: store.snapshot))
|
||||
#expect(TrashModel.canDeleteImmediately(selection: store.selection, in: store.snapshot))
|
||||
#expect(ItemPath.resolve(store.selection.ids, in: .trash, snapshot: store.snapshot)
|
||||
.map { $0.folder(under: store.rootURL).lastPathComponent } == [Ident.indexless])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user