The pro-m1 foundation card. SwiftGitX 0.4.0 (bundled libgit2, the pathfinder's pin) joins the one target; new Kanban/Git/ holds BoardGitMode (pure nearest-.git-wins detection, .git-as-file counts, NSString ancestor walk), HistoryStore (@MainActor @Observable; compose() is the tier gate — free tier gets no object, no detection, no stat), GitRepository (scope-confined SwiftGitX handles: create = init + HEAD forced to main + whole-tree "Initial board state" commit; branch reads incl. unborn/detached; path-history ranks), GitIdentity (derived default as a pure function + repo-local config reader — not libgit2's merged ladder), and GitPathHistory (Mutex-guarded lazy ranker). beginSession composes the git state beside the tier and feeds BoardStore.makeIdentityHistoryRanker; git-mode loads pass the git-backed IdentityHistoryRanker to BoardLoader. The popover's git slot resolves a pure five-way matrix: free tier unchanged (absent / BoardGitNote), Pro mode-aware — Add Git on mode none, honest prose on repo-nested, read-only branch line on git. Provider binding unchanged: both tiers still bind native until the undo/redo card. 42 new tests across 8 suites, all repositories built through bundled libgit2; InertGitTests untouched and green. 2194 tests / 375 suites. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
80 lines
3.5 KiB
Swift
80 lines
3.5 KiB
Swift
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 `BoardInfoView`) is SwiftUI rendering and deliberately untested.
|
|
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 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.
|
|
@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 offers add-git, git mode shows the branch")
|
|
func proFollowsTheMode() {
|
|
#expect(BoardGitSection.resolve(tier: .pro, mode: .none, hasGitDirectory: false) == .addGit)
|
|
#expect(BoardGitSection.resolve(tier: .pro, mode: .git, hasGitDirectory: true) == .branch)
|
|
}
|
|
|
|
@Test("A repo-nested board explains itself — the add-git action is absent, not disabled")
|
|
func repoNestedExplainsRatherThanDisables() {
|
|
let section = BoardGitSection.resolve(tier: .pro, mode: .repoNested, hasGitDirectory: false)
|
|
|
|
// The design is insistent here: "not a hidden 'add git' but a short explanation … the option
|
|
// is absent because it *can't* apply, and the UI should teach that rather than look broken"
|
|
// (06 ▸ Rules). A `.addGit` that rendered disabled would satisfy neither half.
|
|
#expect(section == .repoNested)
|
|
#expect(section != .addGit)
|
|
}
|
|
|
|
@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) }
|
|
}
|
|
}
|
|
)
|
|
|
|
#expect(resolved == Set(BoardGitSection.allCases))
|
|
}
|
|
}
|