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
+16 -12
View File
@@ -32,7 +32,7 @@ private func makeBoard() throws -> WriterFixture {
private func makeGitBoard() async throws -> (fixture: WriterFixture, git: HistoryStore, ledger: EchoLedger) {
let fixture = try makeBoard()
let ledger = EchoLedger()
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro, ledger: ledger))
let git = HistoryStore.compose(boardRoot: fixture.root, ledger: ledger)
#expect(await git.addGit())
return (fixture, git, ledger)
}
@@ -805,7 +805,7 @@ struct AutoCommitContentionTests {
encoding: .utf8
)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
let committer = try quickCommitter(git)
@@ -902,29 +902,33 @@ struct FlushBeforeOverwriteTests {
}
}
// MARK: - Composition and the tier gate
// MARK: - Composition
@MainActor
@Suite("Auto-commit ▸ composition")
struct AutoCommitCompositionTests {
@Test("The free tier composes no committer, because it composes no git state at all")
func theFreeTierHasNoCommitter() throws {
@Test("A board carrying a `.git` composes a committer — there is no tier to compose under")
func aGitBearingBoardComposesACommitter() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
// The tier gate is one level up: no `HistoryStore` means no committer, nothing to disable,
// and no path by which a free-tier session could touch `.git` (12-editions.md).
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
// **PIVOT 2026-08-07** (12-editions.md git left the paywall). This test used to assert the
// gate one level up: `compose(tier: .free)` was `nil`, so there was no committer, nothing to
// disable and no path by which a free session could touch `.git`. What decides now is the
// board's own mode, and this board has a `.git` at its root.
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
#expect(git.committer != nil, "the committer's existence is exactly mode == .git")
}
@Test("A Pro board without a repository has no committer either")
@Test("A board without a repository has no committer")
func modeNoneHasNoCommitter() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .none)
#expect(git.committer == nil, "the committer's existence is exactly mode == .git")
}
@@ -934,7 +938,7 @@ struct AutoCommitCompositionTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
var wired = 0
git.activateAutoCommit { _ in wired += 1 }
#expect(git.committer == nil)
@@ -949,7 +953,7 @@ struct AutoCommitCompositionTests {
let (fixture, _, _) = try await makeGitBoard()
defer { fixture.tearDown() }
let reopened = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let reopened = HistoryStore.compose(boardRoot: fixture.root)
let committer = try #require(reopened.committer)
// Composition happens on the board-open path, where arming a debounce would be a side effect
// of *detection*. `activateAutoCommit` is what starts it.
+1 -1
View File
@@ -719,7 +719,7 @@ struct BoardDecisionSurfaceRepairCommitTests {
// The repository, with everything as it stands committed including the broken root, which is
// what makes the repair a real change rather than a fresh file.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro, ledger: EchoLedger()))
let git = HistoryStore.compose(boardRoot: fixture.root, ledger: EchoLedger())
#expect(await git.addGit())
let commitsBefore = try repairHistory(at: fixture.root).count
git.stopAutoCommit()
+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)
}
+63 -61
View File
@@ -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)
}
}
+2 -2
View File
@@ -37,7 +37,7 @@ private func makeBoard() throws -> WriterFixture {
@MainActor
private func makeGitBoard() async throws -> (fixture: WriterFixture, git: HistoryStore) {
let fixture = try makeBoard()
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro, ledger: EchoLedger()))
let git = HistoryStore.compose(boardRoot: fixture.root, ledger: EchoLedger())
#expect(await git.addGit())
await git.refreshBranch()
return (fixture, git)
@@ -1034,7 +1034,7 @@ struct BoardGitBranchSurfaceTests {
/// is that git mode now carries controls rather than a read-only line.
@Test("Git mode still resolves to the branch section — now the one with controls in it")
func gitModeResolvesToTheBranchSection() {
#expect(BoardGitSection.resolve(tier: .pro, mode: .git, hasGitDirectory: true) == .branch)
#expect(BoardGitSection.resolve(mode: .git) == .branch)
}
}
+2 -2
View File
@@ -137,7 +137,7 @@ private func makeWindow() async throws -> Window {
let store = try BoardStore(rootURL: fixture.root)
await store.awaitQuiescence()
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro, ledger: store.echoes))
let git = HistoryStore.compose(boardRoot: fixture.root, ledger: store.echoes)
#expect(await git.addGit())
let committer = try #require(git.committer)
@@ -494,7 +494,7 @@ struct CardSessionStagingWiringTests {
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item(cardPath, Item.rich(order: "1024", title: "Fix login"))
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
return (fixture, try openBoard(model, at: fixture.root))
}
+62 -50
View File
@@ -39,7 +39,7 @@ private func makeBoard() throws -> WriterFixture {
private func makeGitBoard() async throws -> (fixture: WriterFixture, git: HistoryStore, ledger: EchoLedger) {
let fixture = try makeBoard()
let ledger = EchoLedger()
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro, ledger: ledger))
let git = HistoryStore.compose(boardRoot: fixture.root, ledger: ledger)
#expect(await git.addGit())
return (fixture, git, ledger)
}
@@ -998,32 +998,36 @@ struct GitUndoSessionTests {
// MARK: - The provider binding
/// **The provider follows the board, not the tier alone** (re-ruled 2026-07-31 12-editions.md
/// The provider seam; 13-native-undo.md's header; 06-history-undo.md Rules), stated as the
/// matrix it is: **boards without app-managed git repo-nested included bind the native stack in
/// *every* tier** ("an upgrade never removes undo"), and a Pro git board binds the git provider.
/// There is no third answer any more.
/// **The provider follows the board and since PIVOT 2026-08-07, the board alone** (re-ruled
/// 2026-07-31 12-editions.md The provider seam; 13-native-undo.md's header; 06-history-undo.md
/// Rules; then 12 PIVOT 2026-08-07, git leaves the paywall), stated as the matrix it is: a
/// **git-mode board binds the git provider** and **every other board binds the native stack**
/// repo-nested and unverifiable included in *every* tier. There is no third answer and no second
/// axis.
///
/// ### The repo-nested row stopped being an exception
/// ### What the pivot changed here, and what it did not
///
/// `HistoryStore.compose` returns `nil` off Pro, so a free-tier session never detects a mode at all
/// and cannot tell a repo-nested board from a plain one which is not an omission but 12 The free
/// tier and `.git` verbatim: "opening a board that has one (a formerly-subscribed user's board, a
/// 1.x board, **a repo-nested board**) works normally files read and write as on any board,
/// **native undo runs**". Pro used to answer differently on the same board, which made subscribing
/// *remove* Z from it; the re-ruling of 2026-07-31 retired that case outright "leave strictly
/// alone concerns *git*, and this stack never touches git memory-only, journal-free,
/// session-scoped" (13's header) so the two tiers now agree on every board there is, and the tests
/// below pin both halves of that agreement.
/// The 2026-07-31 re-ruling had already taken the tier out of every row but one: it still decided
/// whether git was on the table at all, because `HistoryStore.compose` returned `nil` off Pro, so a
/// free session never detected a mode and could not tell a repo-nested board from a plain one. The
/// pivot removes that last row "every tier composes the git stack on git-mode boards exactly as
/// Pro did" so a free-tier session on a git board now binds the *git* provider, which is the
/// inversion `freeTierBindsGitOnAGitBoard` below pins.
///
/// What survives untouched is the gitless side: a board nobody added git to detects `none` and binds
/// the native stack, for good, because git stays **opt-in per board** (06 Rules; 13's header read
/// through the pivot). And the repo-nested row keeps the 2026-07-31 reasoning that retired it as an
/// exception "leave strictly alone concerns *git*, and this stack never touches git memory-only,
/// journal-free, session-scoped" (13's header).
@MainActor
@Suite("Git undo ▸ which board gets a provider")
struct GitUndoBindingTests {
@Test("Free tier binds the native stack everywhere, git or not")
func freeTierIsNativeEverywhere() async throws {
@Test("Free tier on a git board binds the git provider — git left the paywall")
func freeTierBindsGitOnAGitBoard() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
let (model, tearDown) = try makeModel()
@@ -1033,16 +1037,20 @@ struct GitUndoBindingTests {
let ref = try openBoard(model, at: fixture.root)
let session = try #require(model.session(for: ref))
#expect(session.git == nil, "the free tier composes no git state at all")
#expect(session.history is NativeHistoryProvider)
#expect(session.undoManager.canUndo == false, "empty, not absent")
// **PIVOT 2026-08-07** (12-editions.md), and the exact inversion of what this test used to
// pin: `session.git == nil` and the native stack, because the free tier composed no git state
// at all and this board's `.git` was inert. It is live now, in every tier.
#expect(session.git != nil, "every session composes a git state")
#expect(session.gitMode == .git)
#expect(session.history is GitHistoryProvider)
#expect(session.tier == .free, "recorded, and consulted by nothing on this path")
}
@Test("Pro on a git board binds the git provider")
func proOnAGitBoardBindsGit() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
let (model, tearDown) = try makeModel()
@@ -1055,8 +1063,8 @@ struct GitUndoBindingTests {
#expect(session.history is GitHistoryProvider)
}
@Test("Pro on a mode-none board binds the native stack — an upgrade never removes undo")
func proOnAPlainBoardBindsTheNativeStack() throws {
@Test("A mode-none board binds the native stack — opt-in is what keeps it there")
func aPlainBoardBindsTheNativeStack() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (model, tearDown) = try makeModel()
@@ -1066,14 +1074,16 @@ struct GitUndoBindingTests {
let ref = try openBoard(model, at: fixture.root)
let session = try #require(model.session(for: ref))
// The board is Pro's it composed a git state and detected a mode and the mode is what
// chose the substrate: "gitless boards bind the native undo stack in every tier" (12).
#expect(session.git != nil, "Pro composes a git state even where there is no repository")
// The board composed a git state and detected a mode, and the mode is what chose the
// substrate: "gitless boards bind the native undo stack in every tier" (12). Since PIVOT
// 2026-08-07 the *only* thing keeping a board on this row is that nobody added git to it
// opt-in per board is exactly what the pivot left standing (06 Rules; 13's header).
#expect(session.git != nil, "a git state is composed even where there is no repository")
#expect(session.gitMode == .none)
#expect(session.history is NativeHistoryProvider)
// And it is a *working* stack, not a placeholder: the same command surface a free-tier
// session gets, which is what "a user subscribing relearns nothing" means here.
// And it is a *working* stack, not a placeholder: the same command surface every other
// gitless board gets, which is what "a user relearns nothing" means here.
let log = StepLog()
#expect(session.undoManager.canUndo == false, "empty, not absent")
session.history?.register(log.step("Move Card"))
@@ -1083,8 +1093,8 @@ struct GitUndoBindingTests {
#expect(log.crossings == ["undo Move Card"])
}
@Test("Pro on a repo-nested board binds the native stack — the no-undo case is retired")
func proOnARepoNestedBoardBindsTheNativeStack() throws {
@Test("A repo-nested board binds the native stack — the no-undo case is retired")
func aRepoNestedBoardBindsTheNativeStack() throws {
let outer = try WriterFixture()
defer { outer.tearDown() }
// A repository at the *parent*, with the board inside it the nested posture.
@@ -1101,15 +1111,15 @@ struct GitUndoBindingTests {
let ref = try openBoard(model, at: boardRoot)
let session = try #require(model.session(for: ref))
// The mode is detected Pro walks the ancestors and it no longer decides Z: "Pro binds it
// on mode-none **and repo-nested** boards alike what repo-nested denies is app-managed
// history, never Z" (13's header, re-ruled 2026-07-31).
// The mode is detected the ancestor walk runs and it no longer decides Z: "mode-none
// **and repo-nested** boards alike bind it what repo-nested denies is app-managed history,
// never Z" (13's header, re-ruled 2026-07-31).
#expect(session.gitMode == .repoNested)
#expect(session.history is NativeHistoryProvider)
#expect(session.undoManager.canUndo == false, "empty, not absent")
// A working stack, exactly as the free tier's on this same board which is the whole point
// of the re-ruling: the two tiers answer alike here now.
// A working stack, exactly as on the same board under any other tier see
// `freeTierOnARepoNestedBoardIsNativeToo`, which now detects the same mode this one does.
let log = StepLog()
session.history?.register(log.step("Move Card"))
#expect(session.undoManager.canUndo)
@@ -1142,12 +1152,12 @@ struct GitUndoBindingTests {
defer { tearDown() }
let git = HistoryStore(boardRoot: fixture.root, mode: .unverifiable, ledger: EchoLedger())
let provider = model.makeHistoryProvider(store, .pro, git)
let provider = model.makeHistoryProvider(store, git)
#expect(provider is NativeHistoryProvider)
}
@Test("The free tier's repo-nested board still binds the native stack — it never detects one")
@Test("The free tier's repo-nested board binds the native stack too — now that it detects one")
func freeTierOnARepoNestedBoardIsNativeToo() throws {
let outer = try WriterFixture()
defer { outer.tearDown() }
@@ -1164,15 +1174,17 @@ struct GitUndoBindingTests {
let ref = try openBoard(model, at: boardRoot)
let session = try #require(model.session(for: ref))
// 12 The free tier and `.git` names this board by hand: "opening a board that has one
// a repo-nested board works normally native undo runs". The tier composes no git state
// at all, so there is nothing here that *could* tell this board from a plain one the inert
// posture made structural rather than remembered.
#expect(session.git == nil)
#expect(session.gitMode == .none, "no detection ran; the session reports the tier's one mode")
// The answer is unchanged and the *reason* is new. Before PIVOT 2026-08-07 the free tier
// composed no git state, so nothing here could tell this board from a plain one and native
// undo followed by default "opening a board that has one a repo-nested board works
// normally native undo runs" (12 The free tier and `.git`, now retired). Detection runs
// in every tier now, sees the enclosing repository, and binds native anyway, because what
// repo-nested denies is app-managed history and never Z (13's header, re-ruled 2026-07-31).
#expect(session.git != nil, "detection runs here now")
#expect(session.gitMode == .repoNested, "and it sees the enclosing repository")
#expect(session.history is NativeHistoryProvider)
// Pro on this same board now answers identically (above) the two tiers agree, which is what
// the re-ruling of 2026-07-31 bought: an upgrade never removes undo from *any* board.
// The same board under `.pro` answers identically (above) one matrix, no tier column.
#expect(session.git?.committer == nil, "and the enclosing repository is still left alone")
}
@Test("Add-git swaps the substrate — the native stack is discarded, the git trail seeded")
@@ -1265,7 +1277,7 @@ struct GitOperationFailureBannerTests {
func aFailedRestorePostsAFailureRow() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
let (model, tearDown) = try makeModel()
@@ -1302,7 +1314,7 @@ struct GitOperationFailureBannerTests {
func theSwitcherReportsFailureAndRecoveryDifferently() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
let (model, tearDown) = try makeModel()
@@ -1336,7 +1348,7 @@ struct GitUndoRoutingTests {
func textEditingWins() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
let (model, tearDown) = try makeModel()
+17 -12
View File
@@ -587,7 +587,7 @@ struct BoardSessionHistoryTests {
defer { tearDown() }
let bound = FakeHistoryProvider()
model.makeHistoryProvider = { _, _, _ in bound }
model.makeHistoryProvider = { _, _ in bound }
let ref = try openBoard(model, at: fixture.root)
let session = try #require(model.session(for: ref))
@@ -597,28 +597,30 @@ struct BoardSessionHistoryTests {
#expect(bound.undoCount == 1, "the window's manager reaches whatever the root bound")
}
@Test("The tier reaches the composition root, and the session records what it composed under")
func theTierIsAComposedFact() throws {
@Test("The session records the tier it composed under — and the provider root never sees it")
func theTierIsARecordedFact() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (model, tearDown) = try makeModel()
defer { tearDown() }
// 12-editions.md The entitlement: the tier is read at composition, once, and handed to the
// root that binds the provider. What this pins is that the *argument arrives* the matrix it
// feeds is `GitUndoBindingTests`', since after the re-ruling of 2026-07-31 the tier decides
// only whether a git state is composed at all, and the mode decides the substrate.
var seen: [Tier] = []
// 12-editions.md The entitlement: the tier is read at composition, once, and recorded.
// **PIVOT 2026-08-07**: it is no longer *handed* anywhere. This test used to pin the argument
// arriving at `makeHistoryProvider` (`{ _, tier, _ in }`); the parameter is gone, so what is
// pinned now is the pair of facts that replaced it the seam takes a store and a git state
// and nothing else, and the session still carries the tier for the base/Pro split yet to be
// ruled. The substrate matrix itself is `GitUndoBindingTests`'.
var seen = 0
model.currentTier = { .pro }
model.makeHistoryProvider = { _, tier, _ in
seen.append(tier)
model.makeHistoryProvider = { _, _ in
seen += 1
return NativeHistoryProvider()
}
let ref = try openBoard(model, at: fixture.root)
let session = try #require(model.session(for: ref))
#expect(seen == [.pro])
#expect(seen == 1, "called once per board, with no tier to consult")
#expect(session.tier == .pro, "the session carries the fact it composed under")
}
@@ -635,7 +637,10 @@ struct BoardSessionHistoryTests {
// The subscription lapses mid-session the one thing 12 The entitlement says must not
// disturb a board that is already on screen: "an open board finishes with the provider it
// composed; the next open composes the native stack over inert `.git`".
// composed". (Its second clause "the next open composes the native stack over inert
// `.git`" went with PIVOT 2026-08-07: the next open composes the git stack too. What is
// pinned here is the recorded fact's immutability, which is what makes the first clause
// true whatever a future split gates.)
model.currentTier = { .free }
#expect(try #require(model.session(for: ref)).tier == .pro)
+62 -52
View File
@@ -4,8 +4,9 @@ import Testing
@testable import Kanban
/// **The board's git state** (06-history-undo.md Rules; 02-architecture.md Components
/// HistoryStore) composed under the tier, detected at open, and changed afterwards by exactly
/// one thing.
/// HistoryStore) composed for every session, detected at open, and changed afterwards by
/// exactly one thing. It was "composed under the tier" until PIVOT 2026-08-07 (12-editions.md git
/// left the paywall); the tier axis is gone from composition and from everything below it.
///
/// Every repository here is a **real** one, made by the app's own add-git through the bundled
/// libgit2: the card's first criterion is that adding git "initializes a repo at the board root with
@@ -40,8 +41,8 @@ private func commitEverything(at boardRoot: URL, message: String) throws {
_ = try repository.commit(message: message)
}
/// Bytes and mtimes of everything under a subtree `InertGitTests`' instrument, in the shape this
/// file needs it: what proves that *reading* a board's mode touched nothing.
/// Bytes and mtimes of everything under a subtree `UntouchedGitTests`' instrument, in the shape
/// this file needs it: what proves that *reading* a board's mode touched nothing.
private struct SubtreeEntry: Equatable {
let path: String
let data: Data?
@@ -102,33 +103,37 @@ private func plantSHA256Repository(in fixture: WriterFixture) throws {
// MARK: - Composition
@MainActor
@Suite("HistoryStore ▸ composition and the tier gate")
@Suite("HistoryStore ▸ composition and open-time detection")
struct HistoryStoreCompositionTests {
@Test("The free tier composes no git state at all, on any board")
func theFreeTierComposesNothing() throws {
@Test("Composition takes no tier: a board carrying a repository opens in git mode, full stop")
func compositionIsUnconditional() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
// Not "mode none on a git board" *nothing*. With no object there is no path by which a
// free-tier session could read history, commit, or touch `.git` (12-editions.md The free
// tier and `.git`, whose byte-level half is `InertGitTests`).
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
// **PIVOT 2026-08-07** (12-editions.md git left the paywall). This was the tier gate's own
// test, and it read the other way: `compose(boardRoot:tier: .free)` answered `nil`, so a free
// session had no git state to consult and never stat'ed a `.git` (the inert posture, made
// structural). The gate is gone the parameter with it and detection now runs on this
// board for every session there is, which is what the assertion below says by having no tier
// to name.
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git, "the `.git` at the root is live, not inert")
}
@Test("Pro on a plain board is mode none — and opening one never creates a repository")
func proOnAPlainBoardIsModeNone() throws {
@Test("A plain board is mode none — and opening one never creates a repository")
func aPlainBoardIsModeNone() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .none)
// "No silent auto-init, ever" (06 Rules) the deliberate pivot from the pathfinder, which
// initialized a repository under every board it opened. Composing twice is the whole test:
// two opens, no repository.
_ = HistoryStore.compose(boardRoot: fixture.root, tier: .pro)
_ = HistoryStore.compose(boardRoot: fixture.root)
#expect(!fixture.exists(".git"), "opening a mode-none board is not an opt-in")
}
@@ -139,14 +144,14 @@ struct HistoryStoreCompositionTests {
// The board a second machine meets: someone ran `git init`/`git clone` (here, the app's own
// add-git in a previous session), and the repository is simply there.
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let first = HistoryStore.compose(boardRoot: fixture.root)
#expect(await first.addGit())
let afterInit = try #require(GitRepository.headCommit(at: fixture.root))
let before = try snapshotGitDirectory(fixture.root)
// The next open. Adoption is not init: no dialog, no confirmation, no second step the mode
// is simply what the filesystem says, and it says git.
let second = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let second = HistoryStore.compose(boardRoot: fixture.root)
#expect(second.mode == .git)
// And nothing happened to the repository on the way in: same HEAD, same bytes, same mtimes.
@@ -165,7 +170,7 @@ struct HistoryStoreCompositionTests {
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try Data(Item.board.utf8).write(to: boardRoot.appendingPathComponent("index.md"))
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(git.mode == .repoNested)
}
@@ -174,7 +179,7 @@ struct HistoryStoreCompositionTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .none)
// What a `git init` in a terminal under an open board does which is nothing, until the
@@ -183,7 +188,7 @@ struct HistoryStoreCompositionTests {
try plantGitDirectory(in: fixture)
#expect(git.mode == .none, "the open session keeps the mode it composed with")
let nextOpen = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let nextOpen = HistoryStore.compose(boardRoot: fixture.root)
#expect(nextOpen.mode == .git, "and the next open reflects what it finds")
}
}
@@ -207,11 +212,11 @@ struct HistoryStoreUnreadableRepositoryTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let first = HistoryStore.compose(boardRoot: fixture.root)
#expect(await first.addGit())
// The next open, which is where the probe actually runs.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
#expect(!git.isRepositoryUnreadable)
#expect(git.committer?.pause == nil)
@@ -226,7 +231,7 @@ struct HistoryStoreUnreadableRepositoryTests {
// filesystem one question, and not a repository at all to libgit2.
try plantGitDirectory(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
// **Never a fall to mode none** "detection is presence-shaped a corrupt or unopenable
// repo never falls to mode none", which is what keeps add-git from ever being offered
@@ -251,7 +256,7 @@ struct HistoryStoreUnreadableRepositoryTests {
defer { fixture.tearDown() }
try plantDanglingGitPointer(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git, "a `.git` file is a repository to git — presence is presence")
#expect(git.isRepositoryUnreadable)
}
@@ -262,7 +267,7 @@ struct HistoryStoreUnreadableRepositoryTests {
defer { fixture.tearDown() }
try plantSHA256Repository(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
// 06 Repository hygiene: "an adopted SHA-256 repo the engine cannot open takes the
// corrupt-repo loud-failure path never a silent fall to mode-none".
@@ -276,7 +281,7 @@ struct HistoryStoreUnreadableRepositoryTests {
try plantGitDirectory(in: fixture)
let before = try snapshotGitDirectory(fixture.root)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.isRepositoryUnreadable)
// "Lanework leaves the repository untouched" the same bytes and the same mtimes, on the
@@ -290,7 +295,7 @@ struct HistoryStoreUnreadableRepositoryTests {
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
await git.writeIdentity(name: "Ada", email: "[email protected]")
#expect(!fixture.exists(".git/config"), "no config was written into a repository nothing can open")
@@ -334,7 +339,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
#expect(fixture.exists(".git"), "a repository at the board root — bundled libgit2, no git install")
@@ -358,7 +363,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The one file the app ever writes into a board because of git, and the one moment it writes
@@ -374,7 +379,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
let derived = GitIdentity.derivedDefault()
@@ -388,7 +393,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// Deterministic rather than inherited: libgit2's initial branch comes from an
@@ -409,7 +414,7 @@ struct HistoryStoreAddGitTests {
// (06 Rules Abnormal repo states: "an unborn HEAD is normal git mode").
_ = try Repository.create(at: fixture.root)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
#expect(GitRepository.branchName(at: fixture.root) != nil)
#expect(GitRepository.headCommit(at: fixture.root) == nil, "no commits yet, and that is fine")
@@ -420,11 +425,11 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let first = HistoryStore.compose(boardRoot: fixture.root)
#expect(await first.addGit())
let head = try #require(GitRepository.headCommit(at: fixture.root))
let second = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let second = HistoryStore.compose(boardRoot: fixture.root)
#expect(await second.addGit() == false, "there is nothing to add")
#expect(GitRepository.headCommit(at: fixture.root)?.oid == head.oid, "and nothing was re-initialized")
}
@@ -439,7 +444,7 @@ struct HistoryStoreAddGitTests {
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try Data(Item.board.utf8).write(to: boardRoot.appendingPathComponent("index.md"))
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(await git.addGit() == false)
#expect(git.mode == .repoNested)
#expect(!FileManager.default.fileExists(atPath: boardRoot.appendingPathComponent(".git").path))
@@ -475,7 +480,7 @@ struct HistoryStoreAddGitTests {
// Composed while the enclosing folder is still a plain one: the store's mode is `none`, and
// that is the reading that goes stale.
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(git.mode == .none)
// A terminal `git init` one level up, after the detection the store is holding.
@@ -530,7 +535,7 @@ struct HistoryStoreAddGitTests {
// Mode is read once, at composition so a store composed before a `.git` appeared still says
// `none` and reaches `create`, which is the layer that refuses. Any refusal will do here; the
// question is where the answer lands.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
try plantGitDirectory(in: fixture)
var banners: [String] = []
git.reportFailure = { banners.append($0.message) }
@@ -563,7 +568,7 @@ struct GitPathHistoryTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// A second card, arriving in a later commit the whole point of the rung.
@@ -587,7 +592,7 @@ struct GitPathHistoryTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let plain = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let plain = HistoryStore.compose(boardRoot: fixture.root)
#expect(plain.identityHistoryRanker == nil, "mode none injects nothing")
// And the free tier has no `HistoryStore` to ask in the first place pinned in the
@@ -600,7 +605,7 @@ struct GitPathHistoryTests {
defer { fixture.tearDown() }
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The same identity, arriving later in a second lane the copy the duplicate rule is about.
@@ -648,32 +653,37 @@ private func openBoard(_ model: AppModel, at url: URL) throws -> BoardWindowRef
@Suite("Board sessions ▸ the git state they compose")
struct BoardSessionGitTests {
@Test("A free-tier session carries no git state, even on a board that has a repository")
func freeSessionsCarryNoGitState() throws {
@Test("A free-tier session composes a git state too, and detects the repository it finds")
func freeSessionsComposeAGitStateAsWell() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let (model, tearDown) = try makeModel()
defer { tearDown() }
// **PIVOT 2026-08-07** (12-editions.md). The inverse of what this test used to pin: the free
// tier carried no git state at all, reported mode `none` on this very board, and injected
// nothing into the loader. Git left the paywall, so the session composes exactly what a Pro
// session composes same call, no tier in it.
model.currentTier = { .free }
let ref = try openBoard(model, at: fixture.root)
let session = try #require(model.session(for: ref))
#expect(session.git == nil)
#expect(session.gitMode == .none, "the free tier ships exactly one mode")
#expect(session.store.makeIdentityHistoryRanker == nil, "and injects nothing into the loader")
#expect(session.git != nil)
#expect(session.gitMode == .git, "the mode is the disk's answer, not the tier's")
#expect(session.store.makeIdentityHistoryRanker != nil, "and the loader's rung is wired")
#expect(session.tier == .free, "the tier is still recorded — it just decides nothing here")
}
@Test("A Pro session on a git board composes git mode and wires the loader's ranker")
func proSessionsCarryTheDetectedMode() async throws {
@Test("A session on a git board composes git mode and wires the loader's ranker")
func sessionsCarryTheDetectedMode() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (model, tearDown) = try makeModel()
defer { tearDown() }
// A real repository, so the ranker has something to read.
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
model.currentTier = { .pro }
@@ -685,13 +695,13 @@ struct BoardSessionGitTests {
let ranker = try #require(provider())
#expect(ranker.rank("\(Ident.lane1)/\(Ident.card1)") != nil)
// The provider binding arrived with the undo/redo card: a Pro session on a git board binds
// the git substrate over exactly this mode (12-editions.md The provider seam).
// The provider binding arrived with the undo/redo card: a session on a git board binds the
// git substrate over exactly this mode (12-editions.md The provider seam).
#expect(session.history is GitHistoryProvider)
}
@Test("A Pro session on a plain board is mode none and injects nothing")
func proSessionsOnPlainBoardsInjectNothing() throws {
@Test("A session on a plain board is mode none and injects nothing")
func sessionsOnPlainBoardsInjectNothing() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (model, tearDown) = try makeModel()
+40 -24
View File
@@ -2,24 +2,37 @@ import Foundation
import Testing
@testable import Kanban
/// **The inert-`.git` posture, stated against real bytes on disk** (12-editions.md The free tier
/// and `.git`): "any `.git` is inert opening a board that has one works normally, but the app
/// never reads history, never commits, never touches `.git` in any way."
/// **The file layer never touches `.git`, stated against real bytes on disk** every write and every
/// load the app makes leaves the whole `.git` subtree byte- and mtime-identical.
///
/// It is unconditional app behaviour, not a build-time posture: the app that ships is the only app
/// there is (12 The target, re-ruled 2026-07-30), and it runs this way for everyone without a
/// subscription and for everyone whose subscription has lapsed "unsubscribed and lapsed are one
/// state". So these tests assert what `Kanban` does, full stop; nothing here is conditioned on a
/// tier, and the git provider that will one day touch `.git` composes over its own boards.
/// ### What this file used to be, and what it is now
///
/// Two halves of that posture are already pinned elsewhere and are referenced, not repeated:
/// `FolderWatcherTests` ".git filtering" proves the watcher ignores churn under a `.git` at any
/// depth, and `BoardLoaderStrayTests.strayFilesAndHiddenEntriesAreIgnoredWithoutWarning` proves a
/// `.git` at the board root loads as an ordinary stray with no warning. Both are *input* claims:
/// what the app does with events and entries it is handed.
/// It was written as the **inert-`.git` posture**'s byte-level proof (12-editions.md The free tier
/// and `.git`, as it then read: "any `.git` is inert the app never reads history, never commits,
/// never touches `.git` in any way"). **PIVOT 2026-08-07** retired that posture outright git left
/// the paywall, so a `.git` at a board root is *live* in every tier, detection runs at every open,
/// and the auto-committer writes into exactly the directory this file's fixture plants.
///
/// This file states the **output** claim, which no existing test covers, and which is the one the
/// posture has to be able to demonstrate: a full session of ordinary editing leaves every byte and
/// The tests survive the retirement unchanged because none of them ever composed a `HistoryStore` or
/// asserted anything about a tier: they drive `BoardWriter` and `BoardLoader` directly, and what they
/// pin is that **those layers are git-agnostic**. That claim is not only still true, it is now
/// load-bearing in two places the retired posture never reached:
///
/// - **A board nobody added git to** (mode `none`) is the one board the app manages no git for, for
/// good git stays opt-in per board (06 Rules; 13-native-undo.md's header read through the
/// pivot). Nothing but this layer runs on such a board, so this layer's indifference *is* the
/// whole promise.
/// - **A `.git` the app does not manage** a repo-nested board's ancestor, a clone dropped inside a
/// card folder (which the fixture below plants deliberately) is left strictly alone by the same
/// indifference, whatever the board's own mode or the user's tier.
///
/// Two neighbouring claims are pinned elsewhere and are referenced, not repeated: `FolderWatcherTests`
/// ".git filtering" proves the watcher ignores churn under a `.git` at any depth, and
/// `BoardLoaderStrayTests.strayFilesAndHiddenEntriesAreIgnoredWithoutWarning` proves a `.git` at the
/// board root loads as an ordinary stray with no warning. Both are *input* claims: what the app does
/// with events and entries it is handed.
///
/// This file states the **output** claim: a full session of ordinary editing leaves every byte and
/// every mtime under `.git` exactly as it found them. It is asserted the only way
/// that is worth anything by snapshotting the whole `.git` subtree from the filesystem before
/// the edits and re-reading it afterwards, never through the app's own read path
@@ -109,9 +122,11 @@ private func makeGitDirectory(in fixture: WriterFixture, under parent: String) t
}
/// The board every test here edits: a root, two lanes, three cards, a `.git` at the board root
/// **and** a second one nested inside a card folder (12: "any `.git` is inert", not just the
/// root's own a repo-nested board or a clone dropped inside a card is the same promise).
private struct InertGitBoard {
/// **and** a second one nested inside a card folder. Both, deliberately: the nested one is a `.git`
/// the app never manages under any ruling (a clone dropped inside a card), and the root one is the
/// case the pivot changed it is a live repository to the *git* layer now, and still nothing at all
/// to the file layer these tests drive.
private struct UntouchedGitBoard {
let fixture: WriterFixture
let laneA: String
let laneB: String
@@ -144,12 +159,13 @@ private struct InertGitBoard {
func tearDown() { fixture.tearDown() }
}
// MARK: - The posture
// MARK: - The claim
struct InertGitTests {
@Suite("The file layer never touches `.git`")
struct UntouchedGitTests {
@Test("A full session of ordinary edits leaves every byte and mtime under .git untouched")
func anEditingSessionNeverTouchesGit() throws {
let board = try InertGitBoard()
let board = try UntouchedGitBoard()
defer { board.tearDown() }
let before = try snapshotSubtree(board.root, ".git")
@@ -205,7 +221,7 @@ struct InertGitTests {
@Test("The board loads and renders normally with a .git at its root, without a warning")
func aGitBearingBoardLoadsLikeAnyOther() throws {
let board = try InertGitBoard()
let board = try UntouchedGitBoard()
defer { board.tearDown() }
let before = try snapshotSubtree(board.root, ".git")
@@ -223,7 +239,7 @@ struct InertGitTests {
@Test("A nested .git rides along a card move byte- and mtime-verbatim")
func aNestedRepositorySurvivesACardMove() throws {
let board = try InertGitBoard()
let board = try UntouchedGitBoard()
defer { board.tearDown() }
let nested = "\(board.laneA)/\(board.cardWithRepo)/.git"
@@ -250,7 +266,7 @@ struct InertGitTests {
@Test("A copy of a card carrying a .git reproduces it verbatim and leaves the original alone")
func aCopyCarriesTheNestedRepositoryWithoutTouchingTheOriginal() throws {
let board = try InertGitBoard()
let board = try UntouchedGitBoard()
defer { board.tearDown() }
let nested = "\(board.laneA)/\(board.cardWithRepo)/.git"
+27 -35
View File
@@ -49,7 +49,7 @@ private func churn(_ fixture: WriterFixture, commits: Int) throws {
// MARK: - Filesystem instruments
/// One entry under a subtree: path, bytes (nil for directories), and mtime `InertGitTests`'
/// One entry under a subtree: path, bytes (nil for directories), and mtime `UntouchedGitTests`'
/// instrument. Bytes alone would pass a rewrite with identical content; the mtime is the assertion
/// that nothing opened the file for writing at all.
private struct SubtreeEntry: Equatable, CustomStringConvertible {
@@ -169,7 +169,7 @@ struct GitignoreSeedTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The file, and the whole of the file the one seed text, shared with board creation and
@@ -192,7 +192,7 @@ struct GitignoreSeedTests {
try fixture.file(".DS_Store", Data([0x00, 0x01, 0x42]))
try fixture.file("\(Ident.lane1)/.DS_Store", Data([0x00, 0x01, 0x43]))
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// Not "removed from history later" never in it. The app has no history-rewriting operation
@@ -211,7 +211,7 @@ struct GitignoreSeedTests {
try fixture.file(".gitignore", mine)
let before = try snapshot(fixture.root, ".gitignore")
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// Not merged, not appended to, not reordered and not even opened for writing, which is
@@ -226,7 +226,7 @@ struct GitignoreSeedTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The user edits it including deleting the line the app seeded, which is their business.
@@ -251,7 +251,7 @@ struct GitignoreSeedTests {
func theCommitterObeysTheSameFile() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The user fine-tunes their own noise definition, which is exactly what the file is for.
@@ -276,7 +276,7 @@ struct GitignoreSeedTests {
// `git init` run outside the app, exactly the shape a cloned or hand-inited board arrives in.
_ = try Repository.create(at: fixture.root)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
#expect(!fixture.exists(".gitignore"), "composing history is not a write")
}
@@ -294,7 +294,7 @@ struct GitignoreSeedTests {
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try Data(Item.board.utf8).write(to: boardRoot.appendingPathComponent("index.md"))
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(await git.addGit() == false)
#expect(git.mode == .repoNested)
#expect(!FileManager.default.fileExists(atPath: boardRoot.appendingPathComponent(".gitignore").path))
@@ -485,7 +485,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 4)
@@ -532,7 +532,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 3)
@@ -554,7 +554,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 3)
@@ -574,7 +574,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
let before = try snapshot(fixture.root, ".git")
@@ -590,7 +590,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 3)
@@ -604,7 +604,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 3)
@@ -624,7 +624,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 3)
@@ -643,7 +643,7 @@ struct GitHousekeepingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
let cardPath = "\(Ident.lane1)/\(Ident.card1)/index.md"
@@ -694,27 +694,19 @@ struct GitHousekeeperSchedulingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let plain = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let plain = HistoryStore.compose(boardRoot: fixture.root)
#expect(plain.housekeeper == nil, "no repository, nothing to maintain")
#expect(await plain.addGit())
#expect(plain.housekeeper != nil, "the mid-session flip maintains itself like any git board")
let reopened = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let reopened = HistoryStore.compose(boardRoot: fixture.root)
#expect(reopened.housekeeper != nil)
}
@Test("The free tier has no housekeeper anywhere, because it has no git state at all")
func theFreeTierMaintainsNothing() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#expect(await seed.addGit())
// Structural, not conditional: with no `HistoryStore` there is no housekeeper to disable and
// no code path that could reach one (12-editions.md The free tier and `.git`).
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
// The mode is the *whole* condition. A companion test used to sit beside this one pinning
// that the free tier maintained nothing anywhere structurally, since `compose` answered
// `nil` off Pro and PIVOT 2026-08-07 (12-editions.md) retired both the gate and the claim:
// there is no tier to compose under, so this suite's one axis is the one above.
}
@Test("A commit in flight defers the pass entirely — it is never retried")
@@ -722,7 +714,7 @@ struct GitHousekeeperSchedulingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
try churn(fixture, commits: 3)
@@ -749,11 +741,11 @@ struct GitHousekeeperSchedulingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
try churn(fixture, commits: 3)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
let housekeeper = try #require(git.housekeeper)
housekeeper.threshold = 1
housekeeper.delay = .milliseconds(20)
@@ -768,7 +760,7 @@ struct GitHousekeeperSchedulingTests {
// Teardown cancels an armed one, so a closed board's maintenance cannot fire against a store
// that has gone.
let second = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let second = HistoryStore.compose(boardRoot: fixture.root)
let secondKeeper = try #require(second.housekeeper)
secondKeeper.threshold = 1
secondKeeper.delay = .milliseconds(200)
@@ -783,7 +775,7 @@ struct GitHousekeeperSchedulingTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
let housekeeper = try #require(git.housekeeper)