import Foundation import Testing @testable import Kanban /// `ClipboardStore`'s own machinery — the manifest, the staging lifecycle, the sweep, the /// changeCount-based takeover, and the deferred cut's arming and voiding (04-interactions.md ▸ /// Clipboard). The *writes* a paste performs live in `PasteWriteTests.swift`. /// /// Every suite here drives a real store over a real temp board, with two things injected: a fake /// pasteboard (so a test never races the machine's one real pasteboard, nor every other test in the /// run) and a temp staging directory (so nothing goes near Application Support). Both seams exist /// exactly because those two claims are the ones worth pinning. // MARK: - Test doubles /// The pasteboard, as a value a test can shove around. /// /// `changeCount` behaves the way `NSPasteboard`'s does — a machine-wide counter that anyone's write /// bumps — because that is the whole basis of takeover detection, and a double that only counted /// *our* writes would make the interesting case untestable. @MainActor final class FakePasteboard: ClipboardPasteboard { private(set) var changeCount = 0 private(set) var text: String? private var data: Data? func manifestData() -> Data? { data } @discardableResult func write(manifest: Data, text: String) -> Int { changeCount += 1 data = manifest self.text = text return changeCount } /// Another app copied: ownership moves, our type is gone, the counter advanced. func takeOver() { changeCount += 1 data = nil text = nil } } // MARK: - Fixtures let clipboardLane1 = ItemID(rawValue: Ident.lane1) let clipboardLane2 = ItemID(rawValue: Ident.lane2) let clipboardCard1 = ItemID(rawValue: Ident.card1) let clipboardCard2 = ItemID(rawValue: Ident.card2) let clipboardCard3 = ItemID(rawValue: Ident.card3) let clipboardCard4 = ItemID(rawValue: Ident.card4) func tombstonedItem(order: String, title: String) -> String { """ --- schema: 1 title: \(title) order: \(order) created: 2026-01-01T09:00:00Z deleted: 2026-03-03T09:00:00Z --- \(title) body. """ } /// Two lanes: `lane1` holds three live cards and one tombstoned one, `lane2` holds a single card. /// `card1` carries two attachments, which is what makes "the snapshot travels whole" and "the /// fallback lost exactly two files" both assertable. @MainActor func makeClipboardBoard() throws -> WriterFixture { let fixture = try WriterFixture() try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo")) try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First")) try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/photo.png", Data("png bytes".utf8)) try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/notes.txt", Data("notes".utf8)) try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second")) try fixture.item("\(Ident.lane1)/\(Ident.card3)", tombstonedItem(order: "3072", title: "Trashed")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing")) try fixture.item("\(Ident.lane2)/\(Ident.card4)", Item.rich(order: "1024", title: "Fourth")) return fixture } /// A store plus the two seams, torn down together. @MainActor struct ClipboardHarness { let fixture: WriterFixture let staging: URL let pasteboard: FakePasteboard let clipboard: ClipboardStore let store: BoardStore init(fixture: WriterFixture) throws { self.fixture = fixture staging = FileManager.default.temporaryDirectory .appendingPathComponent("ClipboardTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true) pasteboard = FakePasteboard() clipboard = ClipboardStore(pasteboard: pasteboard, stagingRoot: staging, observesActivation: false) store = try BoardStore(rootURL: fixture.root) } func tearDown() { try? FileManager.default.removeItem(at: staging) fixture.tearDown() } /// The staged copy directories, sorted — "at most the current copy" is a claim about this list. func stagedCopyIDs() throws -> [String] { try FileManager.default.contentsOfDirectory(atPath: staging.path).sorted() } } @MainActor func makeClipboardHarness() throws -> ClipboardHarness { try ClipboardHarness(fixture: try makeClipboardBoard()) } // MARK: - The manifest @Suite("ClipboardManifest") struct ClipboardManifestTests { private func entry(_ id: String) -> ClipboardManifest.Entry { ClipboardManifest.Entry( id: id, folder: id, title: "First", index: "---\nschema: 1\n---\nbody\n", attachmentCount: 2 ) } @Test("A manifest round-trips through JSON") func roundTrip() throws { let manifest = ClipboardManifest( copyID: "abc", boardRoot: URL(fileURLWithPath: "/tmp/Board.kanban", isDirectory: true), kind: .card, side: .live, entries: [entry(Ident.card1)] ) let data = try #require(manifest.encoded()) #expect(ClipboardManifest(data: data) == manifest) } @Test("A manifest from a future version is refused rather than half-read") func futureVersion() throws { var manifest = ClipboardManifest( copyID: "abc", boardRoot: URL(fileURLWithPath: "/tmp/B", isDirectory: true), kind: .card, side: .live, entries: [entry(Ident.card1)] ) manifest.version = ClipboardManifest.currentVersion + 1 let data = try #require(manifest.encoded()) #expect(ClipboardManifest(data: data) == nil) } @Test("An entryless manifest is nothing to paste") func empty() throws { let manifest = ClipboardManifest( copyID: "abc", boardRoot: URL(fileURLWithPath: "/tmp/B", isDirectory: true), kind: .card, side: .live, entries: [] ) let data = try #require(manifest.encoded()) #expect(ClipboardManifest(data: data) == nil) } @Test("A lane entry's lost-attachment count totals its cards'") func lostAttachments() { let lane = ClipboardManifest.Entry( id: Ident.lane1, folder: Ident.lane1, title: "Todo", index: "---\nschema: 1\n---\n", attachmentCount: 0, cards: [ .init(id: Ident.card1, title: "First", index: "a", attachmentCount: 2), .init(id: Ident.card2, title: "Second", index: "b", attachmentCount: 1), ] ) #expect(lane.lostAttachmentCount == 3) } @Test("The plain-text rendering is the titles, untitled items rendered as the board renders them") func plainText() { var titled = entry(Ident.card1) var untitled = entry(Ident.card2) untitled.title = nil titled.title = "First" let manifest = ClipboardManifest( copyID: "abc", boardRoot: URL(fileURLWithPath: "/tmp/B", isDirectory: true), kind: .card, side: .live, entries: [titled, untitled] ) #expect(manifest.plainText == "First\nUntitled") } } // MARK: - Copy and staging @MainActor @Suite("ClipboardStore ▸ copy") struct ClipboardCopyTests { @Test("A copy stages the whole card folder, attachments and all") func stagesAttachments() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) await harness.clipboard.stagingSettled() let manifest = try #require(harness.clipboard.payload) let staged = harness.staging .appendingPathComponent(manifest.copyID, isDirectory: true) .appendingPathComponent(Ident.card1, isDirectory: true) #expect(FileManager.default.fileExists(atPath: staged.appendingPathComponent("index.md").path)) #expect(try Data(contentsOf: staged.appendingPathComponent("attachments/photo.png")) == Data("png bytes".utf8)) #expect(try Data(contentsOf: staged.appendingPathComponent("attachments/notes.txt")) == Data("notes".utf8)) } @Test("The manifest records identity, side, kind and the source board root") func manifestShape() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1, clipboardCard2], liveness: .live) harness.clipboard.copy(from: harness.store) let manifest = try #require(harness.clipboard.payload) #expect(manifest.kind == .card) #expect(manifest.side == .live) #expect(manifest.rootURL.path == harness.fixture.root.path) // Flatten order — lane `order`, then card `order`. #expect(manifest.entries.map(\.id) == [Ident.card1, Ident.card2]) #expect(manifest.entries.map(\.title) == ["First", "Second"]) #expect(manifest.entries[0].attachmentCount == 2) #expect(manifest.entries[1].attachmentCount == 0) #expect(harness.pasteboard.text == "First\nSecond") } @Test("The embedded index text is the file's bytes, verbatim") func embeddedIndexIsSourceBytes() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) let manifest = try #require(harness.clipboard.payload) let onDisk = try harness.fixture.indexText("\(Ident.lane1)/\(Ident.card1)") #expect(manifest.entries[0].index == onDisk) } @Test("A lane copy embeds its live cards and leaves the tombstoned one out") func laneEntryEmbedsLiveCards() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardLane1], liveness: .live) harness.clipboard.copy(from: harness.store) let manifest = try #require(harness.clipboard.payload) #expect(manifest.kind == .lane) #expect(manifest.entries.map(\.id) == [Ident.lane1]) #expect(manifest.entries[0].cards.map(\.id) == [Ident.card1, Ident.card2]) #expect(manifest.entries[0].lostAttachmentCount == 2) } @Test("A trashed selection copies out, side recorded") func trashedSide() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.transient.isTrashVisible = true harness.store.select([clipboardCard3], liveness: .trashed) harness.clipboard.copy(from: harness.store) let manifest = try #require(harness.clipboard.payload) #expect(manifest.side == .trashed) #expect(manifest.entries.map(\.id) == [Ident.card3]) } @Test("An empty selection copies nothing and leaves the pasteboard alone") func emptySelection() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.clipboard.copy(from: harness.store) #expect(harness.pasteboard.changeCount == 0) #expect(harness.clipboard.payload == nil) } @Test("The store holds at most the current copy — a second copy sweeps the first") func atMostTheCurrentCopy() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) await harness.clipboard.stagingSettled() let first = try #require(harness.clipboard.payload?.copyID) harness.store.select([clipboardCard2], liveness: .live) harness.clipboard.copy(from: harness.store) await harness.clipboard.stagingSettled() let second = try #require(harness.clipboard.payload?.copyID) #expect(first != second) #expect(try harness.stagedCopyIDs() == [second]) } } // MARK: - The sweep @MainActor @Suite("ClipboardStore ▸ sweep") struct ClipboardSweepTests { @Test("A launch sweep collects every tree the pasteboard no longer names") func launchSweepPurgesOrphans() async throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let staging = FileManager.default.temporaryDirectory .appendingPathComponent("ClipboardTests-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: staging) } // Two trees from a previous launch, and a pasteboard that names neither. for orphan in ["one", "two"] { try FileManager.default.createDirectory( at: staging.appendingPathComponent(orphan, isDirectory: true), withIntermediateDirectories: true ) } let clipboard = ClipboardStore( pasteboard: FakePasteboard(), stagingRoot: staging, observesActivation: false ) await clipboard.stagingSettled() #expect(try FileManager.default.contentsOfDirectory(atPath: staging.path).isEmpty) } @Test("A sweep keeps the tree the pasteboard still names") func sweepKeepsTheCurrentCopy() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } try FileManager.default.createDirectory( at: harness.staging.appendingPathComponent("stale", isDirectory: true), withIntermediateDirectories: true ) harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) await harness.clipboard.stagingSettled() let current = try #require(harness.clipboard.payload?.copyID) #expect(try harness.stagedCopyIDs() == [current]) } @Test("A takeover makes our own tree an orphan, and the next sweep collects it") func takeoverOrphansOurSnapshot() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) await harness.clipboard.stagingSettled() #expect(try harness.stagedCopyIDs().count == 1) harness.pasteboard.takeOver() harness.clipboard.sweep() await harness.clipboard.stagingSettled() #expect(try harness.stagedCopyIDs().isEmpty) } } // MARK: - Takeover @MainActor @Suite("ClipboardStore ▸ takeover") struct ClipboardTakeoverTests { @Test("A changeCount that moved without us is a takeover: the payload goes") func payloadClears() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) #expect(harness.clipboard.payload != nil) harness.pasteboard.takeOver() harness.clipboard.refresh() #expect(harness.clipboard.payload == nil) #expect(harness.clipboard.canPaste(into: harness.store) == false) } @Test("An unchanged changeCount is one read and no decode") func refreshIsGuarded() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) let before = harness.pasteboard.changeCount harness.clipboard.refresh() harness.clipboard.refresh() // Nothing was written, so nothing moved — and the payload survived the two refreshes intact. #expect(harness.pasteboard.changeCount == before) #expect(harness.clipboard.payload != nil) } @Test("A takeover voids an armed cut and undims its items") func takeoverVoidsTheCut() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.cut(from: harness.store) #expect(harness.store.transient.pendingCut.ids == [clipboardCard1]) harness.pasteboard.takeOver() harness.clipboard.refresh() #expect(harness.store.transient.pendingCut.isEmpty) } } // MARK: - Cut arming @MainActor @Suite("ClipboardStore ▸ cut") struct ClipboardCutTests { @Test("A cut arms the source board's pending set, in flatten order membership") func armsPendingCut() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1, clipboardCard4], liveness: .live) harness.clipboard.cut(from: harness.store) #expect(harness.store.transient.pendingCut.ids == [clipboardCard1, clipboardCard4]) #expect(harness.store.transient.pendingCut.liveness == .live) } @Test("A second copy voids the pending cut — its pasteboard entry has been overwritten") func copyVoidsTheCut() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.cut(from: harness.store) #expect(!harness.store.transient.pendingCut.isEmpty) harness.store.select([clipboardCard2], liveness: .live) harness.clipboard.copy(from: harness.store) #expect(harness.store.transient.pendingCut.isEmpty) } @Test("Deletion voids per item: a tombstoned cut member leaves the pending set on reload") func deletionVoidsPerItem() async throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1, clipboardCard2], liveness: .live) harness.clipboard.cut(from: harness.store) harness.store.delete([clipboardCard1]) harness.store.handleWatcherEvent(.treeChanged(.appMediated)) await harness.store.awaitQuiescence() #expect(harness.store.transient.pendingCut.ids == [clipboardCard2]) } } // MARK: - Availability @MainActor @Suite("ClipboardStore ▸ availability") struct ClipboardAvailabilityTests { @Test("Copy needs a selection that names something the board renders") func copyNeedsASelection() throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } #expect(harness.clipboard.canCopy(from: harness.store) == false) harness.store.select([clipboardCard1], liveness: .live) #expect(harness.clipboard.canCopy(from: harness.store)) } @Test("Copy works on a trashed selection; cut does not") func trashIsCopyOutOnly() throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.transient.isTrashVisible = true harness.store.select([clipboardCard3], liveness: .trashed) #expect(harness.clipboard.canCopy(from: harness.store)) #expect(harness.clipboard.canCut(from: harness.store) == false) } @Test("The read-only lock blocks cut but never copy") func lockBlocksCutOnly() throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.store.enterVanishedRootLock() #expect(harness.clipboard.canCopy(from: harness.store)) #expect(harness.clipboard.canCut(from: harness.store) == false) #expect(harness.clipboard.canPaste(into: harness.store) == false) } @Test("An open inline editor closes all three — the focused-editor rule") func focusedEditorClosesEverything() throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) harness.store.transient.beginRename(of: clipboardCard1, currentTitle: "First") #expect(harness.clipboard.canCopy(from: harness.store) == false) #expect(harness.clipboard.canCut(from: harness.store) == false) #expect(harness.clipboard.canPaste(into: harness.store) == false) } @Test("Paste needs a payload") func pasteNeedsAPayload() throws { let harness = try makeClipboardHarness() defer { harness.tearDown() } #expect(harness.clipboard.canPaste(into: harness.store) == false) harness.store.select([clipboardCard1], liveness: .live) harness.clipboard.copy(from: harness.store) #expect(harness.clipboard.canPaste(into: harness.store)) } @Test("On a zero-lane board a card payload disables paste and a lane payload does not") func zeroLaneBoard() async throws { let source = try makeClipboardBoard() defer { source.tearDown() } let empty = try WriterFixture() defer { empty.tearDown() } try empty.item("", Item.board) let staging = FileManager.default.temporaryDirectory .appendingPathComponent("ClipboardTests-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: staging) } let clipboard = ClipboardStore( pasteboard: FakePasteboard(), stagingRoot: staging, observesActivation: false ) let sourceStore = try BoardStore(rootURL: source.root) let emptyStore = try BoardStore(rootURL: empty.root) sourceStore.select([clipboardCard1], liveness: .live) clipboard.copy(from: sourceStore) #expect(clipboard.canPaste(into: emptyStore) == false) sourceStore.select([clipboardLane1], liveness: .live) clipboard.copy(from: sourceStore) #expect(clipboard.canPaste(into: emptyStore)) } } // MARK: - The paste anchors @MainActor @Suite("PasteTarget") struct PasteTargetTests { private func snapshot(_ fixture: WriterFixture) throws -> BoardModel { try BoardLoader.load(boardRoot: fixture.root).model } @Test("A card payload lands after the anchor card") func afterTheAnchorCard() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) let target = PasteTarget.cards( selection: ItemReferenceSet(ids: [clipboardCard1], liveness: .live), lastActiveLaneID: nil, snapshot: model ) #expect(target == PasteTarget.Cards(laneID: clipboardLane1, index: 1)) } @Test("A selected lane appends to its bottom") func appendsToASelectedLane() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) let target = PasteTarget.cards( selection: ItemReferenceSet(ids: [clipboardLane1], liveness: .live), lastActiveLaneID: nil, snapshot: model ) // Two rendered cards — the tombstoned third is not in the layout. #expect(target == PasteTarget.Cards(laneID: clipboardLane1, index: 2)) } @Test("A multi-selection anchors at its last member in flatten order") func flattenOrderAnchor() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) let target = PasteTarget.cards( selection: ItemReferenceSet(ids: [clipboardCard4, clipboardCard1], liveness: .live), lastActiveLaneID: nil, snapshot: model ) // `card4` is in the second lane, so it is last in flatten order however the set is spelled. #expect(target == PasteTarget.Cards(laneID: clipboardLane2, index: 1)) } @Test("A tombstoned selection never anchors: it behaves as nothing selected") func tombstonedSelectionNeverAnchors() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) let target = PasteTarget.cards( selection: ItemReferenceSet(ids: [clipboardCard3], liveness: .trashed), lastActiveLaneID: clipboardLane2, snapshot: model ) // The last-active lane, appended — never `card3`'s live disk-lane. #expect(target == PasteTarget.Cards(laneID: clipboardLane2, index: 1)) } @Test("Nothing selected and no last-active lane falls back to the first lane") func firstLaneFallback() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) let target = PasteTarget.cards(selection: .empty, lastActiveLaneID: nil, snapshot: model) #expect(target == PasteTarget.Cards(laneID: clipboardLane1, index: 2)) } @Test("A zero-lane board has no card target at all") func zeroLaneBoardHasNoCardTarget() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let model = try snapshot(fixture) #expect(PasteTarget.cards(selection: .empty, lastActiveLaneID: nil, snapshot: model) == nil) } @Test("A lane payload lands after the anchor lane") func afterTheAnchorLane() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) #expect(PasteTarget.lanes( selection: ItemReferenceSet(ids: [clipboardLane1], liveness: .live), snapshot: model ) == 1) } @Test("A selected card names its lane for a lane paste") func aSelectedCardNamesItsLane() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) #expect(PasteTarget.lanes( selection: ItemReferenceSet(ids: [clipboardCard1], liveness: .live), snapshot: model ) == 1) } @Test("Nothing (or something tombstoned) selected lands a lane at the board's right end") func rightEnd() throws { let fixture = try makeClipboardBoard() defer { fixture.tearDown() } let model = try snapshot(fixture) #expect(PasteTarget.lanes(selection: .empty, snapshot: model) == 2) #expect(PasteTarget.lanes( selection: ItemReferenceSet(ids: [clipboardCard3], liveness: .trashed), snapshot: model ) == 2) } @Test("A zero-lane board still has a lane slot — position zero") func zeroLaneBoardStillTakesALane() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let model = try BoardLoader.load(boardRoot: fixture.root).model #expect(PasteTarget.lanes(selection: .empty, snapshot: model) == 0) } } // MARK: - The degraded paste's phrasing @Suite("BannerCenter ▸ degraded paste") struct DegradedPasteBannerTests { @Test("04's own example sentence") func theExampleSentence() { #expect(BannerCenter.degradedPasteMessage( for: [.init(title: "Fix login", attachments: 3)] ) == "Pasted 'Fix login' without its 3 attachments") } @Test("One attachment is singular") func singular() { #expect(BannerCenter.degradedPasteMessage( for: [.init(title: "Fix login", attachments: 1)] ) == "Pasted 'Fix login' without its attachment") } @Test("An untitled item is 'the item', never the Untitled rendering") func untitled() { #expect(BannerCenter.degradedPasteMessage( for: [.init(title: nil, attachments: 2)] ) == "Pasted the item without its 2 attachments") } @Test("Several items total their attachments rather than listing titles") func several() { #expect(BannerCenter.degradedPasteMessage( for: [.init(title: "A", attachments: 2), .init(title: "B", attachments: 3)] ) == "Pasted 2 items without their 5 attachments") } @Test("Nothing lost says nothing") func nothingLost() { #expect(BannerCenter.degradedPasteMessage(for: []) == nil) #expect(BannerCenter.degradedPasteMessage(for: [.init(title: "A", attachments: 0)]) == nil) } @Test("Posting an empty loss list adds no row") @MainActor func postingNothing() { let center = BannerCenter() center.postDegradedPaste([]) #expect(center.signposts.isEmpty) } }