import Foundation import Testing @testable import Kanban /// A card window's whole lifecycle is one decision re-taken on every snapshot: does this key still /// name a card? Four answers, three of which are "no" for different reasons, and the one that is /// easiest to get wrong — a live card under a tombstoned lane — is invisible in the card's own data. /// So the decision is a pure function and this is its suite; nothing here needs a window. // MARK: - Fixtures /// - lane 1 (live): one live card, one tombstoned card /// - lane 2 (**tombstoned**): one live card, whose own flag is clear @MainActor private func makeBoard() 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: "Fix login")) try fixture.item( "\(Ident.lane1)/\(Ident.card2)", "---\nschema: 1\norder: 2048\ntitle: Gone\ndeleted: 2026-01-01T00:00:00Z\n---\nbody\n" ) try fixture.item( Ident.lane2, "---\nschema: 1\norder: 2048\ntitle: Archive\ndeleted: 2026-01-01T00:00:00Z\n---\nbody\n" ) try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Buried alive")) return fixture } private func title(_ fate: CardWindowFate) -> String? { guard case let .shows(card) = fate else { return nil } return card.title.value } // MARK: - Tests @MainActor @Suite("Card window fate") struct CardWindowFateTests { @Test("A live card in a live lane keeps its window") func aLiveCardShows() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model let fate = CardWindowHost.cardWindowFate(cardID: Ident.card1, in: snapshot) #expect(title(fate) == "Fix login") } @Test("A tombstoned card dismisses its window") func aTombstonedCardDismisses() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model // ⌫ on the board closes the card's open window — "a tombstone counts as deleted" // (05-card-window.md). The card is still in the snapshot; the trash renders it. #expect(snapshot.lanes[0].cards.contains { $0.id.rawValue == Ident.card2 }) #expect(CardWindowHost.cardWindowFate(cardID: Ident.card2, in: snapshot) == .dismisses) } @Test("A live card under a tombstoned lane dismisses too — liveness is ancestor-walked") func aLaneTombstoneDismissesItsCards() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model // The card's own flag says nothing is wrong. Its lane's does, and 03-board-ui.md collapses a // tombstoned lane to one restorable trash entry — so the card renders nowhere, and a window // onto something that renders nowhere is the case this walk exists for. let buried = try #require(snapshot.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.first) #expect(!buried.isDeleted) #expect(CardWindowHost.cardWindowFate(cardID: Ident.card3, in: snapshot) == .dismisses) } @Test("A card that is not in this board's snapshot dismisses — the cross-board move") func anAbsentCardDismisses() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model // What a cross-board move looks like from the source board: the UUID travels with the card, // but the board half of the window's key no longer names it, so the window goes exactly as it // would for a delete. #expect(CardWindowHost.cardWindowFate(cardID: Ident.card4, in: snapshot) == .dismisses) } @Test("A case-respelled card id still finds its card") func theCardIDIsComparedAsAUUIDValue() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model // An agent's `uuidgen` prints uppercase (01-storage-format.md § Fractal layout), so a card // window keyed on one spelling must still find a folder written in the other. Comparing the // strings would dismiss a perfectly live card. let fate = CardWindowHost.cardWindowFate(cardID: Ident.card1.uppercased(), in: snapshot) #expect(title(fate) == "Fix login") } }