The popover/sheet split reverses — settings fold into the Git tab, the widget stacks name over branch

The titlebar widget becomes a two-line identity block: the board glyph at
22pt spanning both lines, the title over the branch (git-mode only, smaller
and secondary), the em-dash retired. New Branch… returns to the switch menu
behind a divider, revealing an inline name field — the pre-split shape. The
board settings sheet retires whole: add-git and commit identity render
inline in the Git tab's postures (BoardGitSetup.swift), the availability
rule collapses into BoardGitSetupSection.resolve, and Board ▸ Board
Settings… leaves the menu bar. Where 07's remote/credential setup surfaces
land is deliberately left open — filed on the Redesign board.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 21:48:02 -04:00
parent 99ebb69a1d
commit 7414fc8400
22 changed files with 884 additions and 1259 deletions
+155
View File
@@ -0,0 +1,155 @@
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]
)
}
}