import Foundation import Testing @testable import Kanban /// **The Info tab's two seams** (03-board-ui.md § Board popover ▸ Info tab, settled 2026-08-07), /// pinned the way the popover's other pure seams are: `BoardInfoMetrics` against a loaded fixture /// board, `BoardDiskFootprint` against real bytes on disk. The rows' rendering — labels, the em-dash /// placeholder, the Reveal button — is SwiftUI and deliberately untested. @Suite("Board popover ▸ Info tab metrics") struct BoardInfoTabTests { /// Two live lanes holding three cards (one carrying two attachments), a loose trashed card, and /// a trashed lane holding two more — the shape that exercises every counting rule at once. private func makeFixture() throws -> WriterFixture { let fixture = try WriterFixture() try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Doing")) try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "One")) try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Two")) try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/sketch.png", Data([0xFF, 0xD8])) try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/notes.txt", Data("notes".utf8)) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Done")) try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Three")) try fixture.item(".trash/\(Ident.card4)", Item.rich(order: "1024", title: "Tossed")) try fixture.item( ".trash/\(Ident.lane3)", "---\nschema: 1\ntitle: Shipped\norder: 3072\nkind: lane\n---\n" ) try fixture.item( ".trash/\(Ident.lane3)/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", Item.rich(order: "1024", title: "Held one") ) try fixture.item( ".trash/\(Ident.lane3)/bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", Item.rich(order: "2048", title: "Held two") ) return fixture } @Test("Lanes and cards count the live board only — the welcome-count rule") func lanesAndCardsAreLiveOnly() throws { let fixture = try makeFixture() defer { fixture.tearDown() } let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model) #expect(metrics.lanes == 2) #expect(metrics.cards == 3) } @Test("The trash tail counts freight — loose cards plus every trashed lane's held cards") func trashedCardsCountFreight() throws { let fixture = try makeFixture() defer { fixture.tearDown() } let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model) // One loose trashed card, two held by the trashed lane — the purge-confirm counting rule. #expect(metrics.trashedCards == 3) } @Test("Attachments count the live cards' files") func attachmentsCountLiveCards() throws { let fixture = try makeFixture() defer { fixture.tearDown() } let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model) #expect(metrics.attachments == 2) } @Test("An empty board is all zeros, not a crash or a lie") func emptyBoardIsZeros() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model) #expect(metrics.lanes == 0) #expect(metrics.cards == 0) #expect(metrics.trashedCards == 0) #expect(metrics.attachments == 0) } } /// **The whole-folder walk** — the disk rows exist to agree with Finder's Get Info, so the walk /// must count hidden entries (`.git`, `.trash/`) exactly as it counts the visible board. @Suite("Board popover ▸ Info tab disk footprint") struct BoardDiskFootprintTests { @Test("Files counts every regular file in the tree, hidden entries included") func filesCountsTheWholeFolder() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Doing")) try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "One")) try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/sketch.png", Data([0xFF, 0xD8])) try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8)) try fixture.item(".trash/\(Ident.card2)", Item.rich(order: "1024", title: "Tossed")) let footprint = BoardDiskFootprint.measure(at: fixture.root) // Three index.md files live (board, lane, card), one attachment, one .git file, one trashed // card's index.md — six regular files; directories are containers, not files. #expect(footprint.files == 6) } @Test("Size on disk is at least the logical bytes — allocation rounds up, never down to zero") func bytesCoverTheLogicalSize() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let payload = Data(repeating: 0x2A, count: 4096) try fixture.file("stray.bin", payload) let footprint = BoardDiskFootprint.measure(at: fixture.root) let logical = Int64(payload.count + Data(Item.board.utf8).count) #expect(footprint.bytes >= logical) } @Test("Created and Modified come back from a real folder, and never out of order") func datesAreReadAndOrdered() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let footprint = BoardDiskFootprint.measure(at: fixture.root) let created = try #require(footprint.created) let modified = try #require(footprint.modified) #expect(modified >= created) } @Test("A vanished folder answers empty rather than trapping — the walk has no user to ask") func vanishedFolderAnswersEmpty() { let gone = URL(fileURLWithPath: "/tmp/lanework-info-tab-never-existed-\(UUID().uuidString)") let footprint = BoardDiskFootprint.measure(at: gone) #expect(footprint.files == 0) #expect(footprint.bytes == 0) } }