import AppKit import Foundation import SwiftUI import Testing @testable import Kanban /// **The board strip's render cost, measured on a real hosted board** (`BoardRenderMetrics`, /// RENDER-INSTRUMENTATION.md). /// /// Ported from the pathfinder's `BoardRenderPerformanceTests`, and the same instrument: a whole /// `BoardView` in an off-screen `NSWindow`, driven exactly the way `FolderWatcher` drives it — one /// `handleWatcherEvent(.treeChanged(.foreign))` per step — with `BoardRenderMetrics` read around /// each. What is asserted is not a timing (a timing is a fact about this machine) but the two /// **invariants the 2026-07-31 drag-performance work is a claim about**: /// /// 1. **A reload that landed value-equal re-runs zero bodies.** Since 988a724 an equal walk skips /// the snapshot assignment entirely (`landedReloads` moves, `snapshotGeneration` does not), so /// this holds by construction rather than by a gate — and pinning it here is what makes the /// *construction* checkable: a future edit that assigned an equal model back would break this /// test rather than quietly costing a whole-board render pass on every `.git` touch. /// 2. **A one-card edit re-renders a handful of bodies, not the board.** This is the equatable /// gates' invariant (84f909a; `ViewEquatableTests` pins the comparison list, this pins the /// effect), and the one that a gate silently coming undone would break. /// /// ...and the counter-invariant that keeps the gates honest: **selecting a card must still repaint /// it**. A gate that suppressed that would be a broken board, not a fast one. /// /// ### What the first run found (2026-08-01) /// /// Invariant 1 holds exactly. Invariant 2 holds for **cards** and fails for **containers**: a /// one-card edit costs 1 card body out of 180, and one lane body per lane on the board — 6 of 6, and /// 12 of 12 when the board is doubled. The cause is not the gate coming undone; it is /// `LaneView.headerInk` reading `store.snapshot.background`, which under Observation's /// whole-property tracking subscribes every lane body to the entire snapshot. /// `theLaneCostFollowsTheBoard` pins both halves — the comparison is right, and it is never asked — /// and the container budget in `aOneCardEditIsNotAWholeBoardRebuild` is written to the number the /// tree actually produces, with the reason, rather than to the pathfinder's 4. /// /// ### What is hosted /// /// The **whole `BoardView`**, in a 1600×1000 off-screen window, with a real `AppModel` in the /// environment (its registry file and clipboard staging redirected into the test's own temp folder, /// `AppModelTests`' idiom). Not a lane-strip stand-in: the gates exist because `BoardView`'s body /// reads the drag session and therefore re-runs for reasons that have nothing to do with any one /// lane, and a harness that hosted `LaneView` directly would be asserting about a parent that does /// not exist. The scaffolding turned out to be four values — the window closure, a /// `TrashConfirmations`, an `openCard` closure and a `BoardSearchPresentation` — all of which a test /// can supply honestly. /// /// Boards are real loads off real temp trees, `ViewEquatableTests`' reason. // MARK: - Fixture /// The board's shape. Wide enough that "the whole board rebuilt" and "one card repainted" are /// unmistakably different numbers, small enough that hosting it stays a unit test. private let laneCount = 6 private let cardsPerLane = 30 /// Ids are derived rather than drawn from `Ident`, because this fixture needs 180 of them. private func laneName(_ lane: Int) -> String { String(format: "1%07d-1111-4111-8111-111111111111", lane) } private func cardName(_ lane: Int, _ card: Int) -> String { String(format: "2%03d%04d-2222-4222-8222-222222222222", lane, card) } @MainActor private func makeFixture(lanes laneCount: Int = laneCount, cards cardsPerLane: Int = cardsPerLane) throws -> WriterFixture { let fixture = try WriterFixture() try fixture.board(title: "Perf Board") for lane in 0.. HostedBoard { let scratch = FileManager.default.temporaryDirectory .appendingPathComponent("BoardRenderPerf-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: scratch, withIntermediateDirectories: true) return HostedBoard(store: try BoardStore(rootURL: fixture.root), scratch: scratch) } /// The counters, snapshotted — reading them into a value keeps a `#expect` from racing a later body. private struct Cost { var strips: Int var cards: Int var containers: Int var measures: Int var cacheHits: Int var sizeThatFits: Int var places: Int @MainActor init() { strips = BoardRenderMetrics.stripBodyEvaluations cards = BoardRenderMetrics.cardBodyEvaluations containers = BoardRenderMetrics.containerBodyEvaluations measures = BoardRenderMetrics.masonryMeasurements cacheHits = BoardRenderMetrics.masonryCacheHits sizeThatFits = BoardRenderMetrics.masonrySizeThatFitsCalls places = BoardRenderMetrics.masonryPlaceCalls } var summary: String { "\(strips) strip bodies, \(containers) container bodies, \(cards) card bodies, " + "\(measures) measures (+\(cacheHits) cached) over " + "\(sizeThatFits) sizeThatFits / \(places) place" } } // MARK: - The invariants @MainActor @Suite("The board strip's render cost", .serialized) struct BoardRenderPerformanceTests { @Test("The counters see a real hosted board — the harness itself, before anything is asserted on it") func theHarnessActuallyRenders() throws { let fixture = try makeFixture() defer { fixture.tearDown() } BoardRenderMetrics.reset() let board = try host(fixture) let first = Cost() withExtendedLifetime(board) {} // A zero here would make every other assertion in this file vacuous — the invariants below // are all "≤", and a harness that rendered nothing satisfies them perfectly. #expect(first.cards >= laneCount * cardsPerLane, "the first paint drew \(first.cards) of \(laneCount * cardsPerLane) card faces") #expect(first.containers >= laneCount) #expect(first.measures > 0, "the masonry measured nothing") print("── first paint — \(first.summary)") } @Test("A reload that landed value-equal re-runs zero bodies") func aValueEqualReloadCostsNothing() async throws { let fixture = try makeFixture() defer { fixture.tearDown() } let board = try host(fixture) let generationBefore = board.store.snapshotGeneration let landedBefore = board.store.landedReloads BoardRenderMetrics.reset() await board.reload() let idle = Cost() print("── reload, nothing changed — \(idle.summary)") // The walk happened and found the board it already had (988a724). #expect(board.store.landedReloads == landedBefore + 1, "the reload did not land") #expect(board.store.snapshotGeneration == generationBefore, "a value-equal reload moved the snapshot generation") #expect(idle.cards == 0, "an unchanged reload re-rendered \(idle.cards) card bodies") #expect(idle.containers == 0, "an unchanged reload re-rendered \(idle.containers) container bodies") } @Test("A one-card edit re-renders a handful of bodies, not the board") func aOneCardEditIsNotAWholeBoardRebuild() async throws { let fixture = try makeFixture() defer { fixture.tearDown() } let board = try host(fixture) let total = laneCount * cardsPerLane // A foreign edit to exactly one card, the way an external editor makes one per keystroke. try fixture.card( cardName(3, 17), in: laneName(3), order: "\(18 * 1024)", title: "Card 3-17 edited", body: "Body text for card 3-17, now edited." ) BoardRenderMetrics.reset() await board.reload() let edit = Cost() print("── reload, ONE card edited — \(edit.summary)") #expect(board.store.snapshotGeneration > 0, "the edit never landed") // **The card gate's invariant, at the pathfinder's budget exactly.** Loose enough to absorb // SwiftUI evaluating a body more than once per update (the first paint runs each face three // times), and two orders of magnitude under the \(total) an un-gated tree costs — which is // what this measured before `CardFaceView.==`. #expect(edit.cards <= 8, "a one-card external edit re-rendered \(edit.cards) of \(total) card faces") // **The container budget is `laneCount`, not the pathfinder's 4 — and that is a diagnosed // finding, not a slack threshold.** Every lane's body re-runs on every model-changing reload, // and `LaneView.==` never gets a say, because `LaneView.body` reads `store.snapshot`: // // LaneView.body → header → .boardTextInk(headerInk) → headerInk // → BoardTextInk.scheme(forBoardBackground: store.snapshot.background, …) // // Observation tracks whole *properties*, so reading `.background` off `store.snapshot` // subscribes that body to the entire snapshot. A reload that changes one card anywhere // assigns `store.snapshot` and invalidates every lane on the board **directly** — and a // direct invalidation is precisely what `.equatable()` has no say over (`LaneView.==`'s own // doc comment says so; the gate still does its job on every *parent-driven* pass, which is // what `aValueEqualReloadCostsNothing` measures at 0 containers for a strip pass that did // happen). // // `theLaneCostFollowsTheBoard` below pins both halves of that diagnosis. Fixing it means // resolving the board's ink once in `BoardView` and passing it down as a compared parameter, // the way `slotWidth` and `columns` already are — out of scope for an instrumentation card, // and the budget here is written to the number the tree actually produces so the *card* gate // stays assertable in the meantime. #expect(edit.containers <= laneCount, "a one-card external edit re-rendered \(edit.containers) bodies for \(laneCount) containers") } @Test("Every lane re-runs on a one-card edit, and not because its gate compared unequal") func theLaneCostFollowsTheBoard() async throws { let wide = laneCount * 2 let fixture = try makeFixture(lanes: wide, cards: 15) defer { fixture.tearDown() } let board = try host(fixture) // An untouched sibling's value, either side of an edit to a different lane. let siblingBefore = try #require(board.store.snapshot.lanes.first { $0.id == ItemID(rawValue: laneName(5)) }) try fixture.card( cardName(3, 7), in: laneName(3), order: "\(8 * 1024)", title: "Card 3-7 edited", body: "Body text for card 3-7, now edited." ) BoardRenderMetrics.reset() await board.reload() let edit = Cost() let siblingAfter = try #require(board.store.snapshot.lanes.first { $0.id == ItemID(rawValue: laneName(5)) }) print("── reload, ONE card edited, \(wide) lanes — \(edit.summary)") // **Half one of the diagnosis: the gate's comparison is right.** An untouched sibling lane is // the same `Lane` value across the reload, and every other member `LaneView.==` compares — // `columns`, `slotWidth`, the store, the band, the drop context — is a window-lived constant // here. So the gate would suppress, if it were ever asked. #expect(siblingBefore == siblingAfter, "an untouched lane came back from the reload unequal") // **Half two: it is never asked.** Doubling the lane count doubles the container cost, which // is the signature of "every lane once" rather than "the edited lane several times". The // cause is `LaneView.headerInk`'s `store.snapshot` read — see // `aOneCardEditIsNotAWholeBoardRebuild` for the chain. // // Deliberately `>=`, as a tripwire in *both* directions: this failing because the number went // **down** means the snapshot read has been hoisted out of `LaneView` and the container // budget above should come down to the pathfinder's 4. #expect(edit.containers >= wide, "a one-card edit cost \(edit.containers) container bodies on a \(wide)-lane board — if that is below \(wide), the headerInk finding is fixed") // The card gate is unaffected by any of it, on a board twice as wide. #expect(edit.cards <= 8, "a one-card edit re-rendered \(edit.cards) of \(wide * 15) card faces") } @Test("Selecting a card still repaints it — the gate never went too far") func selectionStillRepaints() throws { let fixture = try makeFixture() defer { fixture.tearDown() } let board = try host(fixture) BoardRenderMetrics.reset() board.store.select([ItemID(rawValue: cardName(3, 17))], in: .board) board.settle() let selected = Cost() print("── select one card — \(selected.summary)") // Selection changes this face's *styling* (03-board-ui.md § Card face) — something has to // run. The other half of the gate: too strict a `==` would show up here as a zero. #expect(selected.cards > 0, "selecting a card repainted nothing") // What it actually costs is **every face on the board**, and that is the design rather than a // defect: `CardFaceView.body` reads `store.selection` (`isSelected`), so a selection change // invalidates all of them directly — the Observation half the gates explicitly do not cover. // Recorded here as a number rather than asserted as a budget: narrowing it would mean each // face taking its own selected-ness as a compared parameter, which is a design change and not // this card's. See RENDER-INSTRUMENTATION.md ▸ What the first run found. #expect(selected.strips >= 1, "the strip did not re-run for a selection change") } }