import Foundation import Testing @testable import Kanban /// **The board settings sheet's two pure seams** (03-board-ui.md ▸ Board settings sheet, ruled /// 2026-07-31 — the popover/sheet split; 04-interactions.md ▸ The map's configuration carve-out): /// what the sheet holds for a board, and therefore whether the sheet exists for that board at all. /// /// They are pinned here for `BoardGitSection.resolve`'s reason one surface over: the *inventory* is /// the decision worth asserting and the SwiftUI that renders it is not. Every case below is a plain /// value — no board on disk, no window, no session. @Suite("Board settings sheet ▸ the sections") struct BoardSettingsSectionTests { @Test("Pro, mode none: the sheet is add-git and nothing else") func modeNoneHoldsAddGit() { // "add-git (mode none; opt-in init — 06)" — 03's own first entry for this surface, and the // control that moved here out of the popover with the split. #expect(BoardSettingsSection.resolve(tier: .pro, mode: .none) == [.git]) } @Test("Pro, git mode: branch creation and the commit identity, in that order") func gitModeHoldsCreationAndIdentity() { // "branch creation (switching stays in the popover…)" and "commit identity name/email (06 — // the visibility-scoped 2 s config re-read rides with the fields)". #expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity]) } @Test("Every section is reachable from some posture, and no posture invents one") func theInventoryIsTotal() { let offered = Set( Tier.allCases.flatMap { tier in BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(tier: tier, mode: $0) } } ) #expect(offered == Set(BoardSettingsSection.allCases)) } @Test("Pro, unverifiable: the sheet has nothing to show — structurally like repo-nested") func unverifiableHoldsNothing() { // "Denial is not absence" (06 ▸ Rules ▸ Detection, ruled 2026-07-31): a denied ancestor check // can never be told apart from a repository actually being there, so add-git stays as // unreachable here as it is on a genuinely nested board. #expect(BoardSettingsSection.resolve(tier: .pro, mode: .unverifiable) == []) } @Test("Pro, git mode with an unreadable repository: nothing setup-shaped applies either") func anUnreadableRepositoryHoldsNothing() { // **The corrupt-`.git` loud failure** (06 ▸ Rules, ruled 2026-07-31): both sections here are // *writes* to a repository — a branch created in it, an identity written into its config — // and there is no repository the app can open to write either into. Repo-nested's emptiness, // reached one step further along. #expect(BoardSettingsSection.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true) == []) // …and the mode is still `git` throughout: this is emptiness *within* git mode, never the // fall to mode none that would let add-git be offered against an existing `.git`. #expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity]) } @Test("The sections carry the headers VoiceOver navigates by") func headersAreNamed() { // 10-accessibility.md ▸ Board settings sheet: "titled and sectioned with headers VoiceOver // can navigate by". The strings are the surface's spoken structure, so they are stated once // and pinned once. #expect(BoardSettingsSection.git.title == "Git") #expect(BoardSettingsSection.branch.title == "Branch") #expect(BoardSettingsSection.commitIdentity.title == "Commit Identity") } } /// **Where the sheet can be opened from, tier by mode** — the answer both doors validate on: Board ▸ /// Board Settings…'s `disabled` state, and whether the popover's git section renders its Board /// Settings… row at all. @Suite("Board settings sheet ▸ availability") struct BoardSettingsAvailabilityTests { @Test("The whole matrix: Pro on a none-or-git board, and nowhere else") func theMatrix() { // Pro, and a board whose mode leaves something to set up. #expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .none)) #expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .git)) // **Pro, repo-nested**: "nothing setup-shaped can apply" — no add-git (06's prose, not a // disabled button), no branch of ours to create, no repo-local config of ours to write. The // popover's explanation stands and no door opens. #expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .repoNested)) // **Pro, unverifiable**: the same unreachability, for the denial-not-absence reason — a // denied ancestor check is never distinguishable from a repository actually being there. #expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .unverifiable)) // **Pro, git mode over a repository that will not open**: no door either, so neither the // popover's Board Settings… row nor the menu command offers a surface with nothing on it // (06 ▸ Rules, the corrupt-`.git` loud failure). #expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true)) // **The free tier**: no setup exists there at all (12-editions.md ▸ The free tier and // `.git`), whatever mode a stray value claims — detection never runs off Pro, so the mode is // swept for completeness rather than because it can vary. for mode in BoardGitMode.allCases { #expect( !BoardSettingsAvailability.resolve(tier: .free, mode: mode), "the free tier has no board settings sheet in any mode" ) } } @Test("Reachable means exactly 'has something to show'") func reachabilityIsTheInventory() { // The derivation, not a coincidence: a surface whose whole job is hosting setup controls has // no honest empty state, so the two answers are one answer. pro-m2's sections join the // inventory and this identity keeps holding. for tier in Tier.allCases { for mode in BoardGitMode.allCases { #expect( BoardSettingsAvailability.resolve(tier: tier, mode: mode) == !BoardSettingsSection.resolve(tier: tier, mode: mode).isEmpty ) } } } } /// **The window's sheet flag** (`BoardSettingsPresentation`) — `BoardInfoPresentation`'s sibling, /// with the one thing the popover flag does not carry: the session posture both doors validate on. @MainActor @Suite("Board settings sheet ▸ the window's presentation") struct BoardSettingsPresentationTests { @Test("It starts closed, unadopted, and unreachable") func startsClosed() { let presentation = BoardSettingsPresentation() #expect(presentation.isPresented == false) #expect(presentation.tier == .free) #expect(presentation.git == nil) // The free tier's posture is the safe default for a window whose session has not been adopted // yet: an unreachable sheet, rather than a sheet with no sections in it. #expect(presentation.isReachable == false) #expect(presentation.sections.isEmpty) } @Test("An unreachable board's sheet refuses to present") func presentRefusesWhereUnreachable() { let presentation = BoardSettingsPresentation() presentation.present() #expect(presentation.isPresented == false, "the free tier has no sheet to open") } @Test("Two windows hold their own flags") func perWindow() { // `BoardInfoPresentation`'s rule, restated for the sheet: "two board windows each hold their // own and can never toggle each other's". let first = BoardSettingsPresentation() let second = BoardSettingsPresentation() first.isPresented = true #expect(second.isPresented == false) } @Test("Adopting a Pro git session makes the sheet reachable, and dismissal is idempotent") func adoptingASession() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) // Mode `none` — the add-git posture, which is the sheet's whole job on a board with no // repository yet. let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro)) #expect(git.mode == .none) let presentation = BoardSettingsPresentation() presentation.adopt(tier: .pro, git: git) #expect(presentation.isReachable) #expect(presentation.sections == [.git]) presentation.present() #expect(presentation.isPresented) presentation.dismiss() presentation.dismiss() #expect(presentation.isPresented == false) } @Test("The free tier's session leaves both doors shut, .git on the folder or not") func aFreeTierSessionIsUnreachable() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8)) // `compose` is the tier gate: the free tier gets no git state at all, so there is nothing for // a settings sheet to be about even on a board carrying an inert `.git` — the popover's // one-line Pro pointer is that board's whole story (12-editions.md). #expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil) let presentation = BoardSettingsPresentation() presentation.adopt(tier: .free, git: nil) #expect(presentation.isReachable == false) presentation.present() #expect(presentation.isPresented == false) } }