import Foundation import Testing @testable import Kanban /// **What the popover's Git tab sets up, board by board** (03-board-ui.md ▸ Board popover ▸ Git tab; /// 06-history-undo.md ▸ Rules) — `BoardGitSetupSection.resolve`, the tab's setup inventory, pinned /// for `BoardGitSection.resolve`'s reason one seam over: the *contents* are the decision worth /// asserting and the SwiftUI that renders them is not. Every case below is a plain value — no board /// on disk, no window, no session. /// /// ### This suite is the settings sheet's, inherited /// /// It was `BoardSettingsSectionTests` and `BoardSettingsAvailabilityTests` against the sheet's own /// inventory (2026-07-31 → 2026-08-07). The **2026-08-07 reversal** retired that sheet and rehomed /// its controls into the tab, so the *subject* moved and most of the rules did not: an unreadable /// repository still hosts no setup, repo-nested and unverifiable still host none, and mode `none` /// still means add-git and nothing else. /// /// Two things did change, and both are asserted below rather than described: /// /// - **Branch creation is not in this inventory.** It went back into the switch menu as New Branch… /// with an inline reveal (`BoardGitControls`), which is a daily control rather than a standing /// form — so the git-mode posture's setup half is the commit identity alone. /// - **Emptiness no longer means "no surface".** The sheet's `BoardSettingsAvailability` existed to /// answer "does this surface exist at all", because a sheet with no sections had no honest empty /// state and two doors had to validate before opening it. The tab always exists (every board /// carries all three tabs since 12-editions.md ▸ PIVOT 2026-08-07) and always has a posture to /// render, so an empty inventory now means only "this posture hosts no setup block" — there is no /// door left to disable, and the availability seam retired with the doors. @Suite("Board popover ▸ Git tab ▸ the setup inventory") struct BoardGitSetupSectionTests { @Test("Mode none: add-git and nothing else") func modeNoneHoldsAddGit() { // "add-git (mode none; opt-in init — 06)", rendered under the no-repository note since the // reversal put it back where the pre-split popover had it. Every tier's since // 12-editions.md ▸ PIVOT 2026-08-07, and still an offer rather than an auto-init. #expect(BoardGitSetupSection.resolve(mode: .none) == [.addGit]) } @Test("Git mode: the commit identity — creation is the switch menu's again") func gitModeHoldsTheIdentity() { // "commit identity name/email (06 — the visibility-scoped 2 s config re-read rides with the // fields)". Branch creation was this posture's other section while the sheet existed; the // reversal moved it back into the menu, so it is deliberately absent here. #expect(BoardGitSetupSection.resolve(mode: .git) == [.commitIdentity]) } @Test("Every section is reachable from some posture, and no posture invents one") func theInventoryIsTotal() { let offered = Set(BoardGitMode.allCases.flatMap { BoardGitSetupSection.resolve(mode: $0) }) #expect(offered == Set(BoardGitSetupSection.allCases)) } @Test("Repo-nested: nothing setup-shaped applies — the posture is its explanation") func repoNestedHoldsNothing() { // "not a hidden 'add git' but a short explanation … the option is absent because it *can't* // apply" (06 ▸ Rules). The posture still renders — its note is the whole of it — but there // is no setup block under it. #expect(BoardGitSetupSection.resolve(mode: .repoNested) == []) } @Test("Unverifiable: the same emptiness, for the denial-not-absence reason") 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(BoardGitSetupSection.resolve(mode: .unverifiable) == []) } @Test("Git mode with an unreadable repository: no setup block, but still the branch posture") func anUnreadableRepositoryHoldsNoSetup() { // **The corrupt-`.git` loud failure** (06 ▸ Rules, ruled 2026-07-31): writing an identity is // a *write* into the repository's own config, and there is no repository the app can open to // write it into. #expect(BoardGitSetupSection.resolve(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(BoardGitSetupSection.resolve(mode: .git) == [.commitIdentity]) // The difference the tab can express and the sheet could not: the *posture* is unchanged, so // the board still gets its branch surface, holding and explaining itself // (`BoardGitBranchSurface.unreadableNote`). Only the setup block goes. #expect(BoardGitSection.resolve(mode: .git) == .branch) } @Test("Only the identity block carries a header") func headersAreNamed() { // 10-accessibility.md's navigable-header rule, carried over from the retired sheet's // sections: a heading the VoiceOver rotor jumps to. Add-git has none — it renders directly // under the note that states the posture, and a "Git" header inside the Git tab would be the // surface naming itself twice. #expect(BoardGitSetupSection.commitIdentity.title == "Commit Identity") #expect(BoardGitSetupSection.addGit.title == nil) } } /// The same inventory asked about **a board on disk** — the two ends of the range a real /// `HistoryStore` puts into it, so the seam's inputs are shown to be the ones a composed session /// actually produces rather than values a test invented. /// /// `@MainActor` because composing a `HistoryStore` is (the inventory itself is `nonisolated`, which /// is exactly what the suite above exercises with no actor in sight). @MainActor @Suite("Board popover ▸ Git tab ▸ the setup inventory, against a composed session") struct BoardGitSetupCompositionTests { @Test("A board carrying an unopenable `.git` is a git board with no setup") func anInertGitIsAnUnreadableGit() 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)) // The retired posture, asserted where it used to bite: a `.git` at a board root was **inert** // off Pro — never read, never written, the popover's one-line Pro pointer that board's whole // story. Detection runs at every board open on every tier now (12-editions.md ▸ PIVOT // 2026-08-07), so this composes in git mode with no tier asked for. let git = HistoryStore.compose(boardRoot: fixture.root) #expect(git.mode == .git) // And what these three bytes actually are is a repository libgit2 will not open, so the board // takes **06's corrupt-`.git` posture, not the retired inert one** (06 ▸ Rules, ruled // 2026-07-31): git mode throughout — never a fall to mode none that would offer add-git // against an existing `.git` — with no setup block, because every control there would write // into something the app cannot open. #expect(git.isRepositoryUnreadable) #expect( BoardGitSetupSection.resolve( mode: git.mode, isRepositoryUnreadable: git.isRepositoryUnreadable ) == [] ) } @Test("A fresh mode-none board resolves to the add-git offer") func aFreshBoardOffersAddGit() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) // Composed with no tier in sight: git is tier-independent since 12-editions.md ▸ PIVOT // 2026-08-07, so this is every session's git state, not Pro's. let git = HistoryStore.compose(boardRoot: fixture.root) #expect(git.mode == .none) #expect( BoardGitSetupSection.resolve( mode: git.mode, isRepositoryUnreadable: git.isRepositoryUnreadable ) == [.addGit] ) } }