Files
lanework/KanbanTests/PasteWriteTests.swift
T
rzen 53bc71f7fb Materialize the trash — store, undo, and the container universe
Phase 2 swaps every consumer: Liveness and its ancestor walk are gone,
replaced by ItemContainer — a UUID set plus the container side it
lives on, presence the whole test, one selection boundary instead of
the old liveness law. Deletion stages by place: board cards move to
the trash at a store-minted head rank, trash-side delete is permanent
behind its confirmation, Delete Immediately skips the trash from
anywhere, lane delete captures the subtree and removes the folder.
Restore has no method at all — moveCards resolves members in either
container, so drag-out and cut-paste are the ordinary moves 13 calls
them, registering ordinary Move steps. The delete inverse moves the
card back to its captured lane and rank; redo replays the captured
trash rank, a value the gesture actually wrote; lane undo recreates
the subtree byte-faithfully in session. Purges register nothing —
where 13's trash section contradicts its own Rules on that, Rules
wins, filed for ruling. Staleness collapsed to present-or-absent: a
container is a path, so a foreign restore fails the delete step's
expectation structurally. Legacy tombstones migrate on the loose-file
tail hook, cards oldest-first so minting above top reproduces the
retired newest-first column, lanes returning live, one folded loss
row naming both directions. Put Back, restoreByDrag,
receiveRestoredCards, TrashEntry, and the kind machinery are deleted;
the trash column renders the container correctly with its full face
rework left to phase 3.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 17:47:56 -04:00

740 lines
33 KiB
Swift

import Foundation
import Testing
@testable import Kanban
/// What a paste actually writes (04-interactions.md ▸ Clipboard) — the materialization rules, the
/// armed cut's move, and the anchors applied end to end.
///
/// Like every other write suite here these drive a **real store over a real temp board** and then
/// read back through the loader or the raw bytes, never through a snapshot the store handed out: the
/// interesting claims are about the files — which folder arrived, which UUID was minted, which
/// `deleted:` was stripped, which attachment travelled. `WriterFixture`, `Ident` and `Item` come from
/// `WriterTestSupport.swift`; `FakePasteboard`, `ClipboardHarness` and the board fixture come from
/// `ClipboardTests.swift`.
// MARK: - Helpers
/// The board as the loader sees it — never the store's snapshot, which a paste deliberately does not
/// touch (the one-way flow: the write lands, the watcher reloads).
private func pasted(_ fixture: WriterFixture) throws -> BoardModel {
try BoardLoader.load(boardRoot: fixture.root).model
}
private func lane(_ id: ItemID, in fixture: WriterFixture) throws -> Lane? {
try pasted(fixture).lanes.first { $0.id == id }
}
/// A lane's rendered card titles, in display order.
private func pastedTitles(_ id: ItemID, in fixture: WriterFixture) throws -> [String] {
try lane(id, in: fixture)?.cards.filter { !$0.isDeleted }.compactMap(\.title.value) ?? []
}
/// A lane's rendered card folder names, in display order — identity, where titles cannot tell an
/// original from its copy.
private func pastedIDs(_ id: ItemID, in fixture: WriterFixture) throws -> [String] {
try lane(id, in: fixture)?.cards.filter { !$0.isDeleted }.map(\.id.rawValue) ?? []
}
/// A fresh, empty destination board — one lane holding one card, so an arrival has neighbours.
@MainActor
private func makeDestination() throws -> WriterFixture {
let fixture = try WriterFixture()
try fixture.item("", Item.board)
try fixture.item(Ident.lane4, Item.rich(order: "1024", title: "Inbox"))
try fixture.item("\(Ident.lane4)/\(Ident.indexless)", Item.rich(order: "1024", title: "Resident"))
return fixture
}
private let destinationLane = ItemID(rawValue: Ident.lane4)
// MARK: - Copy materialization
@MainActor
@Suite("Paste ▸ copy from staging")
struct PasteFromStagingTests {
@Test("A pasted card is byte-perfect from the snapshot, attachments and all, under a fresh GUID")
func bytePerfectCopy() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
// Fresh identity — "copies mint fresh ones" (01-storage-format.md).
#expect(arrived != Ident.card1)
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "First"])
// The attachments came with it, byte for byte.
#expect(try destination.data("\(Ident.lane4)/\(arrived)/attachments/photo.png")
== Data("png bytes".utf8))
#expect(try destination.data("\(Ident.lane4)/\(arrived)/attachments/notes.txt")
== Data("notes".utf8))
}
@Test("A copy keeps `created` and takes a fresh `modified` — a duplicate is a fork")
func forkStamps() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
let document = try FrontmatterDocument.parse(destination.indexText("\(Ident.lane4)/\(arrived)"))
#expect(document.created.value == ISO8601DateFormatter().date(from: "2026-01-01T09:00:00Z"))
#expect(document.modified.value != ISO8601DateFormatter().date(from: "2026-02-02T09:00:00Z"))
// The unknown keys and the body rode along untouched.
#expect(document.value(for: "project") != nil)
#expect(document.body.contains("First body"))
}
@Test("The originals stay exactly where they were")
func originalsUntouched() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(clipboardLane1, in: harness.fixture) == ["First", "Second"])
}
@Test("A second paste materializes a second copy")
func secondPasteCopiesAgain() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
target.handleWatcherEvent(.treeChanged(.appMediated))
await target.awaitQuiescence()
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let ids = try pastedIDs(destinationLane, in: destination)
#expect(ids.count == 3)
#expect(Set(ids).count == 3)
}
@Test("Pasting into the source board is the within-board duplicate")
func pasteIntoSource() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: harness.store)?.value
// The copy landed immediately after its own original, which is the anchor rule.
#expect(try pastedTitles(clipboardLane1, in: harness.fixture) == ["First", "First", "Second"])
let ids = try pastedIDs(clipboardLane1, in: harness.fixture)
#expect(Set(ids).count == 3)
}
}
// MARK: - Lane pastes
@MainActor
@Suite("Paste ▸ lanes")
struct PasteLaneTests {
@Test("A pasted lane copy takes fresh GUIDs throughout and carries exactly its cards")
func laneCopyRemintsThroughout() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
let model = try pasted(destination)
#expect(model.lanes.count == 2)
let arrived = try #require(model.lanes.last)
#expect(arrived.id.rawValue != Ident.lane1)
#expect(arrived.title.value == "Todo")
// "A lane carries exactly its cards — the trash is board-level, so there is nothing
// lane-nested to strip or carry" (04 ▸ Drag and drop, resettled 2026-07-28).
#expect(arrived.cards.count == 2)
#expect(arrived.cards.map(\.id.rawValue).allSatisfy { $0 != Ident.card1 && $0 != Ident.card2 })
#expect(try pasted(harness.fixture).trash.count == 1, "and the source's trash is untouched")
}
@Test("A lane paste with nothing selected lands at the board's right end")
func laneLandsAtTheRightEnd() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
#expect(try pasted(destination).lanes.compactMap(\.title.value) == ["Inbox", "Todo"])
}
@Test("A lane pastes onto a board with no lanes at all")
func laneOntoZeroLaneBoard() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let empty = try WriterFixture()
defer { empty.tearDown() }
try empty.item("", Item.board)
let target = try BoardStore(rootURL: empty.root)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
#expect(try pasted(empty).lanes.compactMap(\.title.value) == ["Todo"])
}
@Test("A lane cut-move carries its cards whole, identity and all")
func laneCutMoveCarriesItsCards() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.cut(from: harness.store)
await harness.clipboard.paste(into: target)?.value
let model = try pasted(destination)
let arrived = try #require(model.lanes.first { $0.id == clipboardLane1 })
// Identity travelled, and every card came with it — nothing to strip.
#expect(arrived.cards.count == 2)
#expect(model.trash.isEmpty, "the source board's trash is board-level and stays there")
// The lane left the source board entirely.
#expect(try pasted(harness.fixture).lanes.map(\.id) == [clipboardLane2])
}
@Test("The within-board lane duplicate — paste into the source board")
func withinBoardLaneDuplicate() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: harness.store)?.value
let model = try pasted(harness.fixture)
#expect(model.lanes.compactMap(\.title.value) == ["Todo", "Todo", "Doing"])
let duplicate = try #require(model.lanes.dropFirst().first)
#expect(duplicate.id != clipboardLane1)
#expect(duplicate.cards.count == 2)
}
}
// MARK: - The trash's copy-out
@MainActor
@Suite("Paste ▸ from the trash")
struct PasteFromTrashTests {
@Test("A card copied out of the trash is an ordinary copy of an ordinary card")
func cardCopiesOutOrdinarily() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.transient.isTrashVisible = true
harness.store.select([clipboardCard3], in: .trash)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "Trashed"])
// The original stays in the source trash — a copy is a copy (04 ▸ The trash: "⌘C copies a
// trash card; a live copy lands wherever pasted, like copying out of Finder's Trash").
#expect(try pasted(harness.fixture).trash.map(\.id) == [clipboardCard3])
// And nothing had to be stripped on arrival: a trashed card carries no key at all.
let landed = try #require(try lane(destinationLane, in: destination)?.cards.last)
#expect(landed.deleted.isMissing)
}
/// **Cut in the trash, paste into a lane, is the keyboard-native restore** (04-interactions.md
/// ▸ The trash, resettled 2026-07-28) — "an ordinary folder move", which is exactly what the
/// armed cut already does.
@Test("Cut in the trash and paste into a lane is the keyboard restore — the folder moves")
func cutFromTheTrashIsTheKeyboardRestore() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
harness.store.transient.isTrashVisible = true
harness.store.select([clipboardCard3], in: .trash)
harness.clipboard.cut(from: harness.store)
harness.store.select([clipboardLane2], in: .board)
await harness.clipboard.paste(into: harness.store)?.value
let model = try pasted(harness.fixture)
#expect(model.trash.isEmpty, "the folder left the trash")
let lane = try #require(model.lanes.first { $0.id == clipboardLane2 })
#expect(lane.cards.map(\.id).contains(clipboardCard3), "identity travels — it is a move")
#expect(harness.store.transient.pendingCut.isEmpty, "the cut was consumed")
}
}
// MARK: - The destination's search, and the stale pasteboard
/// Two rules that meet at the same guard.
///
/// **A paste is a user-initiated creation, so it clears the destination's query**
/// (04-interactions.md § Search, stated by mechanism: "⌘N, Return-creation, the header button,
/// empty-space double-click, paste, and Finder file drops alike") — cards and lanes alike, since the
/// clipboard holds one or the other and both mint items on arrival.
///
/// **The pasteboard is re-read lazily, and a stale paste no-ops** (04 ▸ Clipboard, settled):
/// "changeCount is checked on activation, on menu validation, and before paste — no timers … the
/// paste itself re-validates and no-ops — nothing stale ever lands, which is the guarantee that
/// matters". Both checks are pinned here, and the clear rides behind the second of them: a paste that
/// lands nothing clears nothing.
@MainActor
@Suite("Paste ▸ the destination's search and the stale pasteboard")
struct PasteSearchAndStalenessTests {
@Test("A card paste clears the destination board's search")
func aCardPasteClearsTheSearch() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
// "First" would be hidden by this query — exactly the card that must not arrive invisible.
target.searchQuery = "resident"
await harness.clipboard.paste(into: target)?.value
#expect(target.searchQuery.isEmpty)
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "First"])
}
@Test("A lane paste clears it too — the rule is creation, not the payload's kind")
func aLanePasteClearsTheSearch() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane2], in: .board)
harness.clipboard.copy(from: harness.store)
target.searchQuery = "resident"
await harness.clipboard.paste(into: target)?.value
#expect(target.searchQuery.isEmpty)
#expect(try pasted(destination).lanes.count == 2)
}
@Test("Another app taking the pasteboard before ⌘V: the paste is refused outright")
func aTakeoverBeforeThePasteRefuses() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
harness.pasteboard.takeOver()
target.select([destinationLane], in: .board)
target.searchQuery = "resident"
// `refresh()` at the front of the paste sees the moved changeCount, so there is no payload
// and no task at all — the same condition the menu item's enablement reads.
#expect(harness.clipboard.paste(into: target) == nil)
#expect(harness.clipboard.canPaste(into: target) == false)
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident"])
#expect(target.searchQuery == "resident")
}
@Test("Another app taking it while the staging chain settles: the paste re-validates and lands nothing")
func aTakeoverMidPasteLandsNothing() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], in: .board)
target.searchQuery = "resident"
// The gesture passed validation; the takeover lands while the task is still waiting on the
// staging chain, which is the window 04 calls the brief lie. Synchronous, so the task cannot
// have run yet: it can only resume where this test suspends.
let paste = harness.clipboard.paste(into: target)
harness.pasteboard.takeOver()
await paste?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident"])
// Nothing landed, so nothing was created — and the query the user was running stands.
#expect(target.searchQuery == "resident")
}
}
// MARK: - The deferred cut
@MainActor
@Suite("Paste ▸ the deferred cut")
struct PasteCutTests {
@Test("The first armed paste moves the originals and clears the cut")
func armedPasteMoves() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
// Identity travelled.
#expect(try pastedIDs(destinationLane, in: destination) == [Ident.indexless, Ident.card1])
// The attachments came with the folder.
#expect(try destination.data("\(Ident.lane4)/\(Ident.card1)/attachments/photo.png")
== Data("png bytes".utf8))
// The original left the source board.
#expect(try pastedTitles(clipboardLane1, in: harness.fixture) == ["Second"])
#expect(harness.store.transient.pendingCut.isEmpty)
}
@Test("A second paste after an armed cut materializes a copy from staging")
func secondPasteAfterACutCopies() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
target.handleWatcherEvent(.treeChanged(.appMediated))
await target.awaitQuiescence()
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let ids = try pastedIDs(destinationLane, in: destination)
#expect(ids.count == 3)
#expect(ids.contains(Ident.card1))
// The second arrival is a fresh identity, not the moved one seen twice.
#expect(Set(ids).count == 3)
}
@Test("A voided cut downgrades to a copy: the originals stay")
func voidedCutCopies() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
// The source board closes — its store goes, and with it the cut's arming.
harness.store.transient.pendingCut = .empty
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
// A copy: a fresh identity at the destination, and the original still at home.
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
#expect(arrived != Ident.card1)
#expect(try pastedTitles(clipboardLane1, in: harness.fixture) == ["First", "Second"])
}
@Test("Per-item voiding: the paste moves only the survivors")
func survivorsOnly() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1, clipboardCard2], in: .board)
harness.clipboard.cut(from: harness.store)
// One of the two is deleted before the paste: the reload ejects it from the pending cut.
harness.store.delete([clipboardCard1])
harness.store.handleWatcherEvent(.treeChanged(.appMediated))
await harness.store.awaitQuiescence()
#expect(harness.store.transient.pendingCut.ids == [clipboardCard2])
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedIDs(destinationLane, in: destination) == [Ident.indexless, Ident.card2])
// The deleted one stayed behind, in the source board's trash.
#expect(try pasted(harness.fixture).trash.map(\.id).contains(clipboardCard1))
}
@Test("A cut emptied down to nothing is simply void — a paste copies instead")
func emptiedCutIsVoid() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
harness.store.delete([clipboardCard1])
harness.store.handleWatcherEvent(.treeChanged(.appMediated))
await harness.store.awaitQuiescence()
#expect(harness.store.transient.pendingCut.isEmpty)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
// The staged snapshot is still there, so the paste is a copy — content intact.
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "First"])
}
}
// MARK: - The staging-less fallback
@MainActor
@Suite("Paste ▸ the staging-less fallback")
struct PasteFallbackTests {
@Test("A missing snapshot falls back to the embedded index.md, byte-faithfully")
func fallbackWritesTheSourceBytes() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
// The snapshot goes — a swept tree, a full disk, an unreadable container.
let copyID = try #require(harness.clipboard.payload?.copyID)
try FileManager.default.removeItem(
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
let document = try FrontmatterDocument.parse(destination.indexText("\(Ident.lane4)/\(arrived)"))
#expect(document.title.value == "First")
// Content intact: unknown keys, the comment's key, and the body all survived.
#expect(document.value(for: "project") != nil)
#expect(document.value(for: "labels") != nil)
#expect(document.body.contains("First body — with *markdown*"))
// `created` kept, fresh `order`.
#expect(document.created.value == ISO8601DateFormatter().date(from: "2026-01-01T09:00:00Z"))
#expect(document.order.value != 1024)
// Attachments absent — which is exactly what the banner is about to say.
#expect(!destination.exists("\(Ident.lane4)/\(arrived)/attachments"))
}
@Test("A degraded paste banners, naming exactly what was lost")
func fallbackBanners() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
try FileManager.default.removeItem(
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(target.banners.losses.map(\.message) == ["Pasted 'First' without its 2 attachments"])
}
@Test("A fallback that lost nothing says nothing")
func fallbackWithoutAttachmentsIsSilent() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
// `card2` has no attachments, so a fallback loses nothing at all.
harness.store.select([clipboardCard2], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
try FileManager.default.removeItem(
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "Second"])
#expect(target.banners.losses.isEmpty)
}
@Test("A lane's fallback materializes its embedded cards")
func laneFallbackCarriesItsCards() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
try FileManager.default.removeItem(
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pasted(destination).lanes.last)
#expect(arrived.title.value == "Todo")
// Both of the lane's cards.
#expect(arrived.cards.count == 2)
#expect(Set(arrived.cards.compactMap(\.title.value)) == ["First", "Second"])
#expect(target.banners.losses.map(\.message) == ["Pasted 'Todo' without its 2 attachments"])
}
@Test("A trash-sourced fallback materializes an ordinary card — there is no key to strip")
func trashedFallbackIsOrdinary() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.transient.isTrashVisible = true
harness.store.select([clipboardCard3], in: .trash)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
try FileManager.default.removeItem(
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "Trashed"])
}
}
// MARK: - BoardWriter.materializeItem
@Suite("BoardWriter ▸ materializeItem")
struct MaterializeItemTests {
@Test("The supplied bytes land verbatim but for the rewritten order and stamps")
func writesTheSuppliedBytes() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
let id = try BoardWriter.materializeItem(
inParent: fixture.url(Ident.lane1),
indexText: Item.rich(order: "9999", title: "Pasted"),
order: 512
)
let document = try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(id.rawValue)"))
#expect(document.title.value == "Pasted")
#expect(document.order.value == 512)
#expect(document.value(for: "project") != nil)
#expect(document.value(for: "labels") != nil)
// The app-write stamps: `modified` set, `modified-by` cleared.
#expect(document.modified.value != ISO8601DateFormatter().date(from: "2026-02-02T09:00:00Z"))
#expect(document.modifiedBy.isMissing)
// `created` untouched — a paste is a fork.
#expect(document.created.value == ISO8601DateFormatter().date(from: "2026-01-01T09:00:00Z"))
}
@Test("Children are materialized under fresh identities and never rewritten")
func childrenAreVerbatim() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
let id = try BoardWriter.materializeItem(
inParent: fixture.root,
indexText: Item.rich(order: "1024", title: "Lane"),
children: [Item.rich(order: "1024", title: "One"), Item.uneditable],
order: 1024
)
let lane = try #require(try BoardLoader.load(boardRoot: fixture.root).model.lanes.first)
#expect(lane.id == id)
#expect(lane.cards.count == 2)
// An uneditable child arrives exactly as it was — the leniency `copyItem` extends below its
// root, applied here.
let names = try FileManager.default.contentsOfDirectory(atPath: fixture.url(id.rawValue).path)
.filter { $0 != "index.md" }
let odd = try #require(names.first { name in
(try? fixture.indexText("\(id.rawValue)/\(name)")) == Item.uneditable
})
#expect(try fixture.indexText("\(id.rawValue)/\(odd)") == Item.uneditable)
}
@Test("An unparseable root refuses and leaves nothing behind")
func unparseableRootLeavesNoResidue() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
let error = writeFailure {
_ = try BoardWriter.materializeItem(
inParent: fixture.root,
indexText: "no frontmatter here at all\n",
order: 1024
)
}
#expect(error != nil)
#expect(try fixture.entryNames("") == ["index.md"])
}
}