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
169 lines
8.3 KiB
Swift
169 lines
8.3 KiB
Swift
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 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."
|
|
///
|
|
/// 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("Info, Theme, Git — in that order, on every board")
|
|
func theStripIsTheWholeSet() {
|
|
#expect(BoardInfoTab.allCases == [.info, .theme, .git])
|
|
}
|
|
|
|
@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 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)
|
|
}
|
|
}
|