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:
@@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user