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