The git surfaces leave the glass — tab, trail, branch line, and the remote pair, peeled

Step 3 of strategy/01-git-excision.md: the popover strip is Info/Theme/Sync, the titlebar widget says the name alone, the card window's History section and its slot go, Board ▸ Pull/Push comes out with the RemoteCommands scaffold, and View ▸ History re-tags from the commit trail to the deferred foreign-change journal. The Sync placeholder re-annotates to the future ops-based sync service. Four git test suites leave with the surfaces they pinned (BoardGitSetup, BoardInfoPopover, BranchSwitch, GitUndo). The git engine still compiles underneath, unreferenced by UI. 2,890 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 10:52:57 -04:00
parent ae7be98eaa
commit 1d97a2931c
16 changed files with 119 additions and 4525 deletions
-155
View File
@@ -1,155 +0,0 @@
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]
)
}
}
-170
View File
@@ -1,170 +0,0 @@
import Foundation
import Testing
@testable import Kanban
/// **The popover's git slot, posture by posture** (03-board-ui.md Board popover Git tab;
/// 06-history-undo.md Rules).
///
/// `BoardGitSection.resolve` is the whole decision, pulled out as a pure function of the board's
/// **mode** precisely so the matrix is assertable the views it selects are SwiftUI and stay
/// untested.
///
/// **The tier axis came out at 12-editions.md PIVOT 2026-08-07** (03's Git-tab note records the
/// consequence): git left the paywall, so the two free-tier postures `.absent` on an ordinary
/// board and the `.proPointer` on one carrying an inert `.git`, both settled 2026-07-27 described
/// a gate that no longer exists and retired with it. With them went the `.git`-on-disk probe that
/// fed them (`BoardGitNote.hasGitDirectory(at:)`) and the tab-strip membership filter that read
/// `.absent` (`BoardInfoTab.available`, whose suite retired here the same day): detection now runs at
/// every board open on every tier, and the mode is the one input left.
@Suite("Board popover ▸ the git section's posture")
struct BoardGitSectionTests {
@Test("Mode none has no repository to describe, git mode shows the branch")
func thePostureFollowsTheMode() {
#expect(BoardGitSection.resolve(mode: .none) == .noRepository)
#expect(BoardGitSection.resolve(mode: .git) == .branch)
}
@Test("A repo-nested board explains itself — setup is absent, not disabled")
func repoNestedExplainsRatherThanDisables() {
let section = BoardGitSection.resolve(mode: .repoNested)
// The design is insistent here: "not a hidden 'add git' but a short explanation the option
// is absent because it *can't* apply, and the UI should teach that rather than look broken"
// (06 Rules). A posture that rendered a disabled setup control would satisfy neither half
// and since the 2026-07-31 split the same sentence is what keeps the settings sheet itself
// out of reach on such a board (`BoardSettingsAvailabilityTests`).
#expect(section == .repoNested)
#expect(section != .noRepository)
}
@Test("An unverifiable board gets its own posture — structurally like nested, never the same case")
func unverifiableIsItsOwnPostureNotRepoNested() {
// "Denial is not absence" (06 Rules Detection, ruled 2026-07-31): a denied ancestor check
// is not a found repository, so the two must resolve to different cases even though both are
// action-less, prose-only sections.
let section = BoardGitSection.resolve(mode: .unverifiable)
#expect(section == .unverifiable)
#expect(section != .repoNested, "a denial is not a nesting")
#expect(section != .noRepository)
}
/// **A board whose repository will not open keeps the git section it has** (06 Rules, the
/// corrupt-`.git` loud failure, ruled 2026-07-31) the *branch* section, held and explaining
/// itself, never the mode-none posture that would imply a board with no history to have.
///
/// The section case is deliberately blind to readability: what changes on such a board is what
/// the branch surface inside it says (`BoardGitBranchSurface.resolve`, whose broken presentation
/// and own sentence `BoardGitBranchSurfaceTests` pins), not which section the popover shows. The
/// posture matrix stays a function of the mode alone.
@Test("An unreadable repository is still the branch section — never the no-repository posture")
func anUnreadableRepositoryKeepsTheBranchSection() {
let section = BoardGitSection.resolve(mode: .git)
#expect(section == .branch)
#expect(section != .noRepository, "the board has a repository; it is unreadable, not absent")
}
@Test("Every posture is reachable, and none of them is two postures")
func theMatrixIsTotal() {
let resolved = Set(BoardGitMode.allCases.map { BoardGitSection.resolve(mode: $0) })
// One posture per mode, and every posture spoken for which is also what makes the tab
// strip's membership rule trivially true since the pivot: no mode resolves to nothing, so
// the Git tab is never dropped (03-board-ui.md Board popover, the pivot note).
#expect(resolved == Set(BoardGitSection.allCases))
#expect(resolved.count == BoardGitMode.allCases.count, "no two modes share a posture")
}
}
/// **The tab strip carries every tab, always** (03-board-ui.md Board popover, the tabbed-
/// popover paragraph as the 2026-08-07 pivot note leaves it): "the absent posture is unreachable and
/// every board carries all three tabs. The membership rule stands structurally the strip still asks
/// the posture it just never hears 'absent' anymore." (Three became four the same night: **Sync**
/// joined as a standing placeholder, membership still `allCases`.)
///
/// So there is no `available()` seam left to pin; what is worth keeping is the strip's *order*,
/// which the popover's `Picker` takes from `allCases` and which the Git session fixed: Info first
/// it is the board's face and the tab selection resets to it on every open then Theme, then Git,
/// then the Sync placeholder at the end of the line, where a surface with nothing in it belongs.
@Suite("Board popover ▸ the tab strip")
struct BoardInfoTabStripTests {
@Test("Info, Theme, Git, Sync — in that order, on every board")
func theStripIsTheWholeSet() {
#expect(BoardInfoTab.allCases == [.info, .theme, .git, .sync])
}
@Test("The raw values are the segmented control's own labels")
func labelsAreTheRawValues() {
// The strip needs no separate label function, which is the only reason this enum is
// `String`-backed at all.
#expect(BoardInfoTab.allCases.map(\.rawValue) == ["Info", "Theme", "Git", "Sync"])
}
}
/// **The window-title widget's two strings** (03-board-ui.md Board popover, the card that widened
/// the widget from a chevron to the whole board-name area): `BoardInfoTitlebarSummary` is the pure
/// function this pins, exactly as `BoardGitSectionTests` above pins the section it shares its mode
/// input with. No disk I/O the title half of this seam takes a raw `URL`, not a `BoardStore`, so a
/// plain `/tmp/...` path is enough.
///
/// **The tier axis came out at 12-editions.md PIVOT 2026-08-07.** The branch rule read `tier ==
/// .pro && mode == .git` until that day; git is tier-independent now, so a git-mode board shows its
/// branch and nothing else does.
@Suite("Board popover ▸ the widget's strings")
struct BoardInfoTitlebarSummaryTests {
private let root = URL(fileURLWithPath: "/tmp/My Board.board")
@Test("The title is the on-disk title, falling back to the folder name sans extension")
func titleFallsBackToTheFolderName() {
let named = BoardInfoTitlebarSummary(
snapshotTitle: "Sprint 12", rootURL: root, mode: .none, branch: nil
)
#expect(named.title == "Sprint 12")
let untitled = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: .none, branch: nil
)
#expect(untitled.title == "My Board")
let emptyTitled = BoardInfoTitlebarSummary(
snapshotTitle: "", rootURL: root, mode: .none, branch: nil
)
#expect(
emptyTitled.title == "My Board",
"an empty title reads the same as no title at all — the window-title rule this seam restates"
)
}
@Test("The branch shows in git mode, once it has been read")
func branchShowsInGitMode() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: .git, branch: "main"
)
#expect(summary.branch == "main")
}
@Test("No other mode shows a branch, whatever the git state hands it")
func onlyGitModeShowsABranch() {
for mode in BoardGitMode.allCases where mode != .git {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: mode, branch: "main"
)
#expect(
summary.branch == nil,
"mode \(mode): a board the app manages no repository for must never surface a stray branch value"
)
}
}
@Test("A git-mode board whose branch has not been read yet shows none, honestly")
func unreadBranchShowsNone() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, mode: .git, branch: nil
)
#expect(summary.branch == nil)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff