The tier axis comes out of the git stack — compose unconditional, postures mode-driven

HistoryStore.compose(boardRoot📒) returns non-optional and runs for
every session — the nil the gate produced was the only nil it ever had.
makeHistoryProvider is a one-axis decision: git-mode boards bind the git
provider, everything else native, in every tier; Session.tier stays
recorded, dormant. BoardGitSection shrinks to the four mode postures
(.absent and .proPointer die, BoardGitNote and the .git probe with them);
every board carries all three popover tabs (BoardInfoTab.available
retired); the titlebar branch shows on any git-mode board; the settings
sheet and card History section stop reading tier. InertGitTests is
repurposed as UntouchedGitTests — the file layer still never opens .git,
now load-bearing for mode-none boards. The accessibility audit reaches the
settings sheet at last: the fixture board hosts it in every tier, so the
free-fixture disabled-row test becomes an open-and-audit test.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 20:21:28 -04:00
parent da0d7fd2d7
commit 798a8bac73
32 changed files with 830 additions and 904 deletions
+64 -142
View File
@@ -2,63 +2,32 @@ import Foundation
import Testing
@testable import Kanban
/// **The board popover's git-note detection** (12-editions.md The free tier and `.git`, ruled
/// 2026-07-27): the free tier's whole git story is one line, shown only on a board that carries an
/// inert `.git`. The seam
/// that decides *whether* to show it `BoardGitNote.hasGitDirectory(at:)` is a pure, read-only
/// `FileManager` check, pinned here against real bytes on disk. Everything else about the note (its
/// wording, its placement in `BoardGitTabView`) is SwiftUI rendering and deliberately untested.
/// **The popover's git slot, posture by posture** (03-board-ui.md Board popover Git tab;
/// 06-history-undo.md Rules).
///
/// Since the 2026-08-07 tab restructure this same answer decides one thing more: whether the popover
/// carries a Git tab at all on a free board (`BoardInfoTabAvailabilityTests` below).
struct BoardInfoPopoverTests {
@Test("A board with a .git at its root reports true")
func trueForABoardWithGit() 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))
#expect(BoardGitNote.hasGitDirectory(at: fixture.root))
}
@Test("An ordinary board with no .git reports false")
func falseForAnOrdinaryBoard() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
#expect(!BoardGitNote.hasGitDirectory(at: fixture.root))
}
}
/// **The popover's git slot, posture by posture** (03-board-ui.md Board popover; 06-history-undo.md
/// Rules; 12-editions.md The free tier and `.git`).
/// `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.
///
/// `BoardGitSection.resolve` is the whole decision, pulled out as a pure function of the tier and the
/// board's mode precisely so the matrix is assertable the views it selects are SwiftUI and stay
/// untested, exactly as the note's wording and placement do above.
/// **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("The free tier: absent on an ordinary board, a Pro pointer on a board carrying an inert .git")
func theFreeTierIsContextual() {
#expect(BoardGitSection.resolve(tier: .free, mode: .none, hasGitDirectory: false) == .absent)
#expect(BoardGitSection.resolve(tier: .free, mode: .none, hasGitDirectory: true) == .proPointer)
}
@Test("Pro: mode none has no repository to describe, git mode shows the branch")
func proFollowsTheMode() {
#expect(BoardGitSection.resolve(tier: .pro, mode: .none, hasGitDirectory: false) == .noRepository)
#expect(BoardGitSection.resolve(tier: .pro, mode: .git, hasGitDirectory: true) == .branch)
@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(tier: .pro, mode: .repoNested, hasGitDirectory: false)
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"
@@ -74,7 +43,7 @@ struct BoardGitSectionTests {
// "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(tier: .pro, mode: .unverifiable, hasGitDirectory: false)
let section = BoardGitSection.resolve(mode: .unverifiable)
#expect(section == .unverifiable)
#expect(section != .repoNested, "a denial is not a nesting")
@@ -88,10 +57,10 @@ struct BoardGitSectionTests {
/// 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 tier and the mode alone.
/// 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(tier: .pro, mode: .git, hasGitDirectory: true)
let section = BoardGitSection.resolve(mode: .git)
#expect(section == .branch)
#expect(section != .noRepository, "the board has a repository; it is unreadable, not absent")
@@ -99,84 +68,49 @@ struct BoardGitSectionTests {
@Test("Every posture is reachable, and none of them is two postures")
func theMatrixIsTotal() {
let resolved = Set(
Tier.allCases.flatMap { tier in
BoardGitMode.allCases.flatMap { mode in
[true, false].map { BoardGitSection.resolve(tier: tier, mode: mode, hasGitDirectory: $0) }
}
}
)
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's membership** (03-board-ui.md Board popover, the tabbed-popover paragraph
/// the Git session's ruling, 2026-08-07): the Git tab joins the strip only where the git section has
/// something true to say, so a free-tier board with no `.git` shows Info | Theme alone.
/// **The tab strip carries all three tabs, 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."
///
/// `BoardInfoTab.available` is pinned here for the reason `BoardGitSection.resolve` is pinned above:
/// it is the whole decision, pure in its three inputs, and the segmented `Picker` it feeds is SwiftUI
/// and stays untested. What these tests are really about is that membership **is** the posture rather
/// than a second reading of it the last one walks the entire input matrix and asserts the tab's
/// presence against `resolve` itself, so the strip cannot drift from the surface it labels.
@Suite("Board popover ▸ the tab strip's membership")
struct BoardInfoTabAvailabilityTests {
/// 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.
@Suite("Board popover ▸ the tab strip")
struct BoardInfoTabStripTests {
@Test("A free board with no .git shows Info and Theme alone — never a standing Git tab")
func theFreeTierDropsTheGitTab() {
// 12-editions.md's "absent, no placeholder" carried up to the tab strip: a Git tab on every
// free board would be the standing ad for Pro that 12 forbids.
#expect(BoardInfoTab.available(tier: .free, mode: .none, hasGitDirectory: false) == [.info, .theme])
@Test("Info, Theme, Git — in that order, on every board")
func theStripIsTheWholeSet() {
#expect(BoardInfoTab.allCases == [.info, .theme, .git])
}
@Test("A free board carrying an inert .git earns the tab — it has the Pro pointer to say")
func anInertGitEarnsTheTab() {
#expect(BoardInfoTab.available(tier: .free, mode: .none, hasGitDirectory: true) == [.info, .theme, .git])
}
@Test("Pro shows the Git tab under every mode — each of the four postures is a tab surface")
func proAlwaysShowsTheTab() {
for mode in BoardGitMode.allCases {
for hasGit in [true, false] {
#expect(
BoardInfoTab.available(tier: .pro, mode: mode, hasGitDirectory: hasGit)
== [.info, .theme, .git],
"Pro, mode \(mode): the tab is the posture's home, and Git is always last"
)
}
}
}
@Test("The tab is present exactly where the git section is not absent, across the whole matrix")
func membershipDelegatesToThePosture() {
for tier in Tier.allCases {
for mode in BoardGitMode.allCases {
for hasGit in [true, false] {
let tabs = BoardInfoTab.available(tier: tier, mode: mode, hasGitDirectory: hasGit)
let isAbsent =
BoardGitSection.resolve(tier: tier, mode: mode, hasGitDirectory: hasGit) == .absent
#expect(
tabs.contains(.git) == !isAbsent,
"\(tier)/\(mode)/git=\(hasGit): membership is the posture, not a second reading of it"
)
#expect(
Array(tabs.prefix(2)) == [.info, .theme],
"Info and Theme stand on every board, in that order — Info is the board's face"
)
#expect(tabs.count == Set(tabs).count, "no tab appears twice")
}
}
}
@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"])
}
}
/// **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
/// tier/mode inputs 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.
/// 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 {
@@ -185,17 +119,17 @@ struct BoardInfoTitlebarSummaryTests {
@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, tier: .free, mode: .none, branch: nil
snapshotTitle: "Sprint 12", rootURL: root, mode: .none, branch: nil
)
#expect(named.title == "Sprint 12")
let untitled = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .free, mode: .none, branch: nil
snapshotTitle: nil, rootURL: root, mode: .none, branch: nil
)
#expect(untitled.title == "My Board")
let emptyTitled = BoardInfoTitlebarSummary(
snapshotTitle: "", rootURL: root, tier: .free, mode: .none, branch: nil
snapshotTitle: "", rootURL: root, mode: .none, branch: nil
)
#expect(
emptyTitled.title == "My Board",
@@ -203,43 +137,31 @@ struct BoardInfoTitlebarSummaryTests {
)
}
@Test("The branch shows under Pro, in git mode, once it has been read")
func branchShowsForProGitMode() {
@Test("The branch shows in git mode, once it has been read")
func branchShowsInGitMode() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: "main"
snapshotTitle: nil, rootURL: root, mode: .git, branch: "main"
)
#expect(summary.branch == "main")
}
@Test("The free tier never shows a branch, whatever the mode or the git state hands it")
func theFreeTierNeverShowsABranch() {
for mode in BoardGitMode.allCases {
@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, tier: .free, mode: mode, branch: "main"
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"
)
#expect(summary.branch == nil, "detection never runs under the free tier, so a stray branch value must never surface")
}
}
@Test("An inert .git under Pro — mode none, repo-nested, or unverifiable — never shows a branch")
func inertGitNeverShowsABranch() {
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .none, branch: "main").branch == nil
)
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .repoNested, branch: "main").branch
== nil
)
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .unverifiable, branch: "main").branch
== nil
)
}
@Test("A git-mode board whose branch has not been read yet shows none, honestly")
func unreadBranchShowsNone() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: nil
snapshotTitle: nil, rootURL: root, mode: .git, branch: nil
)
#expect(summary.branch == nil)
}