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:
@@ -9,53 +9,54 @@ import Testing
|
||||
/// They are pinned here for `BoardGitSection.resolve`'s reason one surface over: the *inventory* is
|
||||
/// the decision worth asserting and the SwiftUI that renders it is not. Every case below is a plain
|
||||
/// value — no board on disk, no window, no session.
|
||||
///
|
||||
/// **The tier axis came out at 12-editions.md ▸ PIVOT 2026-08-07**: this inventory resolved `guard
|
||||
/// tier == .pro` first and the mode second until git left the paywall. What survives is the mode
|
||||
/// reading, which was always the part carrying the design's reasoning.
|
||||
@Suite("Board settings sheet ▸ the sections")
|
||||
struct BoardSettingsSectionTests {
|
||||
|
||||
@Test("Pro, mode none: the sheet is add-git and nothing else")
|
||||
@Test("Mode none: the sheet is add-git and nothing else")
|
||||
func modeNoneHoldsAddGit() {
|
||||
// "add-git (mode none; opt-in init — 06)" — 03's own first entry for this surface, and the
|
||||
// control that moved here out of the popover with the split.
|
||||
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .none) == [.git])
|
||||
// control that moved here out of the popover with the split. Every tier's since
|
||||
// 12-editions.md ▸ PIVOT 2026-08-07, and still an offer rather than an auto-init.
|
||||
#expect(BoardSettingsSection.resolve(mode: .none) == [.git])
|
||||
}
|
||||
|
||||
@Test("Pro, git mode: branch creation and the commit identity, in that order")
|
||||
@Test("Git mode: branch creation and the commit identity, in that order")
|
||||
func gitModeHoldsCreationAndIdentity() {
|
||||
// "branch creation (switching stays in the popover…)" and "commit identity name/email (06 —
|
||||
// the visibility-scoped 2 s config re-read rides with the fields)".
|
||||
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
|
||||
#expect(BoardSettingsSection.resolve(mode: .git) == [.branch, .commitIdentity])
|
||||
}
|
||||
|
||||
@Test("Every section is reachable from some posture, and no posture invents one")
|
||||
func theInventoryIsTotal() {
|
||||
let offered = Set(
|
||||
Tier.allCases.flatMap { tier in
|
||||
BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(tier: tier, mode: $0) }
|
||||
}
|
||||
)
|
||||
let offered = Set(BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(mode: $0) })
|
||||
|
||||
#expect(offered == Set(BoardSettingsSection.allCases))
|
||||
}
|
||||
|
||||
@Test("Pro, unverifiable: the sheet has nothing to show — structurally like repo-nested")
|
||||
@Test("Unverifiable: the sheet has nothing to show — structurally like repo-nested")
|
||||
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(BoardSettingsSection.resolve(tier: .pro, mode: .unverifiable) == [])
|
||||
#expect(BoardSettingsSection.resolve(mode: .unverifiable) == [])
|
||||
}
|
||||
|
||||
@Test("Pro, git mode with an unreadable repository: nothing setup-shaped applies either")
|
||||
@Test("Git mode with an unreadable repository: nothing setup-shaped applies either")
|
||||
func anUnreadableRepositoryHoldsNothing() {
|
||||
// **The corrupt-`.git` loud failure** (06 ▸ Rules, ruled 2026-07-31): both sections here are
|
||||
// *writes* to a repository — a branch created in it, an identity written into its config —
|
||||
// and there is no repository the app can open to write either into. Repo-nested's emptiness,
|
||||
// reached one step further along.
|
||||
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true) == [])
|
||||
#expect(BoardSettingsSection.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(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
|
||||
#expect(BoardSettingsSection.resolve(mode: .git) == [.branch, .commitIdentity])
|
||||
}
|
||||
|
||||
@Test("The sections carry the headers VoiceOver navigates by")
|
||||
@@ -69,41 +70,34 @@ struct BoardSettingsSectionTests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Where the sheet can be opened from, tier by mode** — the answer both doors validate on: Board ▸
|
||||
/// **Where the sheet can be opened from, mode by mode** — the answer both doors validate on: Board ▸
|
||||
/// Board Settings…'s `disabled` state, and whether the popover's git section renders its Board
|
||||
/// Settings… row at all.
|
||||
///
|
||||
/// **The Pro clause is gone** (12-editions.md ▸ PIVOT 2026-08-07): every board can reach the setup
|
||||
/// its own mode leaves it, on any tier.
|
||||
@Suite("Board settings sheet ▸ availability")
|
||||
struct BoardSettingsAvailabilityTests {
|
||||
|
||||
@Test("The whole matrix: Pro on a none-or-git board, and nowhere else")
|
||||
@Test("The whole matrix: a none-or-git board, and nowhere else")
|
||||
func theMatrix() {
|
||||
// Pro, and a board whose mode leaves something to set up.
|
||||
#expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .none))
|
||||
#expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .git))
|
||||
// A board whose mode leaves something to set up.
|
||||
#expect(BoardSettingsAvailability.resolve(mode: .none))
|
||||
#expect(BoardSettingsAvailability.resolve(mode: .git))
|
||||
|
||||
// **Pro, repo-nested**: "nothing setup-shaped can apply" — no add-git (06's prose, not a
|
||||
// **Repo-nested**: "nothing setup-shaped can apply" — no add-git (06's prose, not a
|
||||
// disabled button), no branch of ours to create, no repo-local config of ours to write. The
|
||||
// popover's explanation stands and no door opens.
|
||||
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .repoNested))
|
||||
#expect(!BoardSettingsAvailability.resolve(mode: .repoNested))
|
||||
|
||||
// **Pro, unverifiable**: the same unreachability, for the denial-not-absence reason — a
|
||||
// denied ancestor check is never distinguishable from a repository actually being there.
|
||||
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .unverifiable))
|
||||
// **Unverifiable**: the same unreachability, for the denial-not-absence reason — a denied
|
||||
// ancestor check is never distinguishable from a repository actually being there.
|
||||
#expect(!BoardSettingsAvailability.resolve(mode: .unverifiable))
|
||||
|
||||
// **Pro, git mode over a repository that will not open**: no door either, so neither the
|
||||
// **Git mode over a repository that will not open**: no door either, so neither the
|
||||
// popover's Board Settings… row nor the menu command offers a surface with nothing on it
|
||||
// (06 ▸ Rules, the corrupt-`.git` loud failure).
|
||||
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true))
|
||||
|
||||
// **The free tier**: no setup exists there at all (12-editions.md ▸ The free tier and
|
||||
// `.git`), whatever mode a stray value claims — detection never runs off Pro, so the mode is
|
||||
// swept for completeness rather than because it can vary.
|
||||
for mode in BoardGitMode.allCases {
|
||||
#expect(
|
||||
!BoardSettingsAvailability.resolve(tier: .free, mode: mode),
|
||||
"the free tier has no board settings sheet in any mode"
|
||||
)
|
||||
}
|
||||
#expect(!BoardSettingsAvailability.resolve(mode: .git, isRepositoryUnreadable: true))
|
||||
}
|
||||
|
||||
@Test("Reachable means exactly 'has something to show'")
|
||||
@@ -111,13 +105,11 @@ struct BoardSettingsAvailabilityTests {
|
||||
// The derivation, not a coincidence: a surface whose whole job is hosting setup controls has
|
||||
// no honest empty state, so the two answers are one answer. pro-m2's sections join the
|
||||
// inventory and this identity keeps holding.
|
||||
for tier in Tier.allCases {
|
||||
for mode in BoardGitMode.allCases {
|
||||
#expect(
|
||||
BoardSettingsAvailability.resolve(tier: tier, mode: mode)
|
||||
== !BoardSettingsSection.resolve(tier: tier, mode: mode).isEmpty
|
||||
)
|
||||
}
|
||||
for mode in BoardGitMode.allCases {
|
||||
#expect(
|
||||
BoardSettingsAvailability.resolve(mode: mode)
|
||||
== !BoardSettingsSection.resolve(mode: mode).isEmpty
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,10 +125,11 @@ struct BoardSettingsPresentationTests {
|
||||
let presentation = BoardSettingsPresentation()
|
||||
|
||||
#expect(presentation.isPresented == false)
|
||||
#expect(presentation.tier == .free)
|
||||
#expect(presentation.git == nil)
|
||||
// The free tier's posture is the safe default for a window whose session has not been adopted
|
||||
// yet: an unreachable sheet, rather than a sheet with no sections in it.
|
||||
// No session adopted yet, so no sheet: every control this surface hosts writes *through* the
|
||||
// git state, and a sheet of bare headers is the empty state the design forbids. (It was the
|
||||
// free tier's default that kept this shut before 12-editions.md ▸ PIVOT 2026-08-07; the
|
||||
// absence of a session is what keeps it shut now.)
|
||||
#expect(presentation.isReachable == false)
|
||||
#expect(presentation.sections.isEmpty)
|
||||
}
|
||||
@@ -146,7 +139,7 @@ struct BoardSettingsPresentationTests {
|
||||
let presentation = BoardSettingsPresentation()
|
||||
|
||||
presentation.present()
|
||||
#expect(presentation.isPresented == false, "the free tier has no sheet to open")
|
||||
#expect(presentation.isPresented == false, "an unadopted window has no sheet to open")
|
||||
}
|
||||
|
||||
@Test("Two windows hold their own flags")
|
||||
@@ -160,19 +153,20 @@ struct BoardSettingsPresentationTests {
|
||||
#expect(second.isPresented == false)
|
||||
}
|
||||
|
||||
@Test("Adopting a Pro git session makes the sheet reachable, and dismissal is idempotent")
|
||||
@Test("Adopting a session makes the sheet reachable, and dismissal is idempotent")
|
||||
func adoptingASession() throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.item("", Item.board)
|
||||
|
||||
// Mode `none` — the add-git posture, which is the sheet's whole job on a board with no
|
||||
// repository yet.
|
||||
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
|
||||
// repository yet. 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)
|
||||
|
||||
let presentation = BoardSettingsPresentation()
|
||||
presentation.adopt(tier: .pro, git: git)
|
||||
presentation.adopt(git: git)
|
||||
|
||||
#expect(presentation.isReachable)
|
||||
#expect(presentation.sections == [.git])
|
||||
@@ -185,23 +179,31 @@ struct BoardSettingsPresentationTests {
|
||||
#expect(presentation.isPresented == false)
|
||||
}
|
||||
|
||||
@Test("The free tier's session leaves both doors shut, .git on the folder or not")
|
||||
func aFreeTierSessionIsUnreachable() throws {
|
||||
@Test("A board carrying a `.git` is a git board — the inert posture is retired")
|
||||
func anInertGitIsNoLongerInert() 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))
|
||||
|
||||
// `compose` is the tier gate: the free tier gets no git state at all, so there is nothing for
|
||||
// a settings sheet to be about even on a board carrying an inert `.git` — the popover's
|
||||
// one-line Pro pointer is that board's whole story (12-editions.md).
|
||||
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
|
||||
// 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 both doors shut because every section here is a write
|
||||
// into something the app cannot open.
|
||||
#expect(git.isRepositoryUnreadable)
|
||||
|
||||
let presentation = BoardSettingsPresentation()
|
||||
presentation.adopt(tier: .free, git: nil)
|
||||
presentation.adopt(git: git)
|
||||
|
||||
#expect(presentation.isReachable == false)
|
||||
presentation.present()
|
||||
#expect(presentation.isPresented == false)
|
||||
#expect(presentation.sections.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user