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