An unopenable repository fails loudly — the standing row, the paused surface, the honest heal

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-06 18:44:12 -04:00
parent 52df210284
commit 0d8ecdb78b
20 changed files with 938 additions and 42 deletions
+60
View File
@@ -732,6 +732,66 @@ struct AutoCommitContentionTests {
#expect(committer.commitCount == 0)
}
/// **The corrupt-`.git` loud failure's engine half** (06-history-undo.md Rules, ruled
/// 2026-07-31): "the board itself loads and edits normally files are the board but the
/// failure is loud with the whole git surface paused", and "the banner clears when a later
/// open or reload finds the repo readable".
@Test("An unreadable repository holds the engine, and heals when it opens again")
func anUnreadableRepositoryHolds() async throws {
let (fixture, git, _) = try await makeGitBoard()
defer { fixture.tearDown() }
let committer = try quickCommitter(git)
var transitions: [Bool] = []
committer.reportRepositoryUnreadable = { transitions.append($0) }
// What a half-copied or half-deleted `.git` looks like to libgit2: the layout no longer
// validates, so the repository will not open at all. Reversible, which is what makes the
// heal half of this test the real thing rather than a second fixture.
let head = fixture.root.appendingPathComponent(".git/HEAD")
let savedHead = try Data(contentsOf: head)
try FileManager.default.removeItem(at: head)
try fixture.item("\(Ident.lane1)/\(Ident.card2)", plain(order: "2048", title: "Second"))
committer.noteReloadLanded(sawForeignChange: true)
await committer.flushNow()
#expect(committer.pause == .unreadable)
#expect(committer.commitCount == 0, "nothing is attempted against a repository the app can't open")
#expect(committer.lastFailure == nil, "a pause is not a failure")
#expect(transitions == [true], "the banner is raised once, on the transition")
// "Edits keep landing on disk files are the board and commit as one settled batch when
// the state clears." Mid-session, the re-read that notices is the paused engine's own.
try fixture.item("\(Ident.lane1)/card-3", plain(order: "3072", title: "Third"))
try savedHead.write(to: head)
await committer.flushNow()
#expect(committer.pause == nil)
#expect(committer.commitCount == 1, "one settled batch, exactly as any other pause")
#expect(transitions == [true, false], "and the banner heals — it is a condition, not an event")
#expect(isClean(at: fixture.root))
}
@Test("The popover's own re-read learns the state without attempting anything")
func refreshPauseLearnsTheUnreadableRepository() async throws {
let (fixture, git, _) = try await makeGitBoard()
defer { fixture.tearDown() }
let committer = try quickCommitter(git)
let head = fixture.root.appendingPathComponent(".git/HEAD")
let savedHead = try Data(contentsOf: head)
try FileManager.default.removeItem(at: head)
await committer.refreshPause()
#expect(committer.pause == .unreadable)
#expect(committer.commitCount == 0)
try savedHead.write(to: head)
await committer.refreshPause()
#expect(committer.pause == nil)
}
@Test("An unborn HEAD is normal — the first settled change commits the whole tree")
func anUnbornHeadIsNormal() async throws {
let fixture = try makeBoard()
+121 -6
View File
@@ -103,16 +103,20 @@ struct BannerCenterOrderingTests {
suspension: HistorySuspension(reason: "disk full", since: Date(timeIntervalSince1970: 50)),
operations: [operation],
signposts: [signpost],
gitFailures: [restore]
gitFailures: [restore],
repositoryUnreadable: true
)
// in-progress (pinned) > read-only lock > reload breakage > one-shot failures, both shapes >
// loss rows > commit and attachment failures > passive info rows. The two info classes
// sit at opposite ends of the strip.
// in-progress (pinned) > read-only lock > reload breakage > **the unreadable repository** >
// one-shot failures, both shapes > loss rows > commit and attachment failures > passive info
// rows. The two info classes sit at opposite ends of the strip, and the breakage class holds
// two rows now (06-history-undo.md Rules, ruled 2026-07-31): the reload breakage first,
// because it is the one saying the board on screen is not the board on disk.
#expect(rows.map(\.id) == [
"operation:\(operation.id.uuidString)",
"read-only-lock",
"reload-breakage",
"repository-unreadable",
"git-failure:\(restore.id.uuidString)",
"one-shot:\(move.id.uuidString)",
"loss:\(loss.id.uuidString)",
@@ -120,11 +124,46 @@ struct BannerCenterOrderingTests {
"one-shot:\(attachment.id.uuidString)",
"signpost:\(signpost.id.uuidString)",
])
#expect(rows.map(\.tone) == [.info, .error, .error, .error, .error, .warning, .warning, .error, .info])
#expect(rows.map(\.isPinned) == [true, false, false, false, false, false, false, false, false],
#expect(rows.map(\.tone) == [.info, .error, .error, .error, .error, .error, .warning, .warning, .error, .info])
#expect(rows.map(\.isPinned) == [true, false, false, false, false, false, false, false, false, false],
"a spinner may never hide behind '+N more' — nothing else is pinned")
}
/// **The corrupt-`.git` loud failure's row** (06-history-undo.md Rules, ruled 2026-07-31)
/// it stands with the breakage class and above every one-shot, which is what "breakage-class"
/// buys it: a failed move posted a second ago never pushes it down the strip.
@Test("The unreadable repository outranks every failure, and only the breakage class outranks it")
func theUnreadableRepositoryStandsInTheBreakageClass() {
let move = OneShotBanner(error: error(.move(title: "Fix login")))
let rows = BannerCenter.rows(
lock: nil,
breakage: nil,
oneShots: [move],
losses: [],
suspension: nil,
operations: [],
repositoryUnreadable: true
)
#expect(rows.map(\.id) == ["repository-unreadable", "one-shot:\(move.id.uuidString)"])
#expect(rows.first?.tone == .error, "the ruling's word is breakage, and breakage is an error")
}
@Test("A readable repository contributes no row at all")
func aReadableRepositoryIsSilent() {
let rows = BannerCenter.rows(
lock: nil,
breakage: nil,
oneShots: [],
losses: [],
suspension: nil,
operations: [],
repositoryUnreadable: false
)
#expect(rows.isEmpty)
}
@Test("Both failure shapes share one rank, interleaved by recency")
func theFailureRankHoldsBothShapes() {
// "Failures rank by what they are, not by which error vocabulary threw them" (02 § The
@@ -432,6 +471,51 @@ struct BannerCenterLifecycleTests {
).isEmpty)
}
/// **The corrupt-`.git` loud failure** (06-history-undo.md Rules, ruled 2026-07-31): the row
/// is raised at detection, stands with no dismiss, and *heals* "the banner clears when a later
/// open or reload finds the repo readable".
@Test("The unreadable repository is a standing condition that heals, never a dismissable row")
func theUnreadableRepositoryIsAHealingCondition() throws {
let center = BannerCenter()
#expect(!center.isRepositoryUnreadable)
center.raiseRepositoryUnreadable()
#expect(center.isRepositoryUnreadable)
let rows = BannerCenter.rows(
lock: nil, breakage: nil, oneShots: [], losses: [], suspension: nil, operations: [],
repositoryUnreadable: center.isRepositoryUnreadable
)
#expect(rows.count == 1)
#expect(rows[0].tone == .error)
#expect(rows[0].dismissID == nil, "a condition is never dismissed while it is still true")
// 06's own sentence, verbatim the three clauses being what is wrong, what it costs, and
// the promise that makes waiting safe.
#expect(rows[0].headline
== "This board's git repository can't be read — history is paused; Lanework leaves the repository untouched")
#expect(rows[0].headline == BannerCenter.repositoryUnreadableMessage)
center.clearRepositoryUnreadable()
#expect(!center.isRepositoryUnreadable)
#expect(BannerCenter.rows(
lock: nil, breakage: nil, oneShots: [], losses: [], suspension: nil, operations: [],
repositoryUnreadable: center.isRepositoryUnreadable
).isEmpty)
}
@Test("Raising and clearing are idempotent — a re-read that confirms the condition changes nothing")
func raisingTheUnreadableRepositoryIsIdempotent() {
let center = BannerCenter()
center.raiseRepositoryUnreadable()
center.raiseRepositoryUnreadable()
#expect(center.isRepositoryUnreadable)
center.clearRepositoryUnreadable()
center.clearRepositoryUnreadable()
#expect(!center.isRepositoryUnreadable)
}
@Test("Re-suspending keeps the original start and takes the newer diagnosis")
func resuspendingKeepsTheClock() throws {
let center = BannerCenter()
@@ -604,11 +688,13 @@ struct BannerRowControlsTests {
let rows: [BannerRow] = [
.readOnlyLock(.vanishedRoot),
.reloadBreakage(BoardLoadFailure(BoardLoadError(path: "Todo/index.md", reason: .missingOrder))),
.repositoryUnreadable,
.historySuspended(HistorySuspension(reason: "the repository is corrupt")),
]
for row in rows {
#expect(row.controls.isEmpty, "\(row.id) is a condition — it heals, it is not waved away")
#expect(row.dismissID == nil)
}
}
@@ -937,4 +1023,33 @@ struct BannerCenterStoreTests {
"signpost:\(store.banners.signposts[0].id.uuidString)",
])
}
/// **The row the git state raises, through the store** (06-history-undo.md Rules, the
/// corrupt-`.git` loud failure, ruled 2026-07-31) raised and healed by
/// `noteRepositoryUnreadable(_:)`, which is the seam `AppModel.beginSession` wires the
/// committer's pause transitions to, and **announced** both ways per 10-accessibility.md.
@Test("The unreadable repository stands on the strip and is spoken when it appears and clears")
func theUnreadableRepositoryRowIsRaisedAndSpoken() throws {
let fixture = try makeBoardWithUneditableLane()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
var spoken: [String] = []
store.announce = { if let line = $0 { spoken.append(line) } }
store.noteRepositoryUnreadable(true)
#expect(store.bannerRows.map(\.id) == ["repository-unreadable"])
#expect(store.bannerRows[0].headline == BannerCenter.repositoryUnreadableMessage)
#expect(spoken == ["Error: \(BannerCenter.repositoryUnreadableMessage)"],
"a standing banner is announced when it appears — the row's own sentence, tone first")
// The 15 s re-read confirming what is already standing must not say it again.
store.noteRepositoryUnreadable(true)
#expect(spoken.count == 1)
store.noteRepositoryUnreadable(false)
#expect(store.bannerRows.isEmpty)
#expect(spoken.last == "History is recording again")
}
}
+44
View File
@@ -441,6 +441,50 @@ struct BoardAnnouncerSpeechTests {
)
}
/// **The corrupt-`.git` loud failure, spoken** (06-history-undo.md Rules, ruled 2026-07-31:
/// "announced per 10-accessibility.md"). It ranks last of the raised conditions, matching the
/// strip's own precedence: the two above it describe the board's files, this one the history
/// over them.
@Test("The unreadable repository announces on arrival, under the conditions about the files")
func raisedRepositoryUnreadable() {
var facts = BoardAnnouncer.ReloadFacts()
facts.repositoryUnreadableAfter = true
#expect(
BoardAnnouncer.speech(for: facts)
== AccessibilityPhrases.bannerLabel(
tone: .error,
headline: BannerCenter.repositoryUnreadableMessage
)
)
// A breakage standing beside it leads: the board on screen not being the board on disk is
// the more consequential of the two.
facts.breakageAfter = breakage()
#expect(
BoardAnnouncer.speech(for: facts)
== AccessibilityPhrases.bannerLabel(tone: .error, headline: BannerCenter.headline(for: breakage()))
)
}
@Test("A repository that heals is announced too, as the regained capability")
func clearedRepositoryUnreadable() {
var facts = BoardAnnouncer.ReloadFacts()
facts.repositoryUnreadableBefore = true
facts.repositoryUnreadableAfter = false
#expect(BoardAnnouncer.speech(for: facts) == "History is recording again")
}
@Test("A standing unreadable repository is not repeated on every re-read")
func standingRepositoryUnreadableIsNotRepeated() {
var facts = BoardAnnouncer.ReloadFacts()
facts.repositoryUnreadableBefore = true
facts.repositoryUnreadableAfter = true
#expect(BoardAnnouncer.speech(for: facts) == nil, "the 15 s re-read confirms; it does not narrate")
}
@Test("A cleared lock is announced — the banner speaks when it clears, not only when it appears")
func clearedLock() {
var facts = BoardAnnouncer.ReloadFacts()
+16
View File
@@ -78,6 +78,22 @@ struct BoardGitSectionTests {
#expect(section != .noRepository)
}
/// **A board whose repository will not open keeps the git section it has** (06 Rules, the
/// corrupt-`.git` loud failure, ruled 2026-07-31) the *branch* section, held and explaining
/// itself, never the mode-none posture that would imply a board with no history to have.
///
/// 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.
@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)
#expect(section == .branch)
#expect(section != .noRepository, "the board has a repository; it is unreadable, not absent")
}
@Test("Every posture is reachable, and none of them is two postures")
func theMatrixIsTotal() {
let resolved = Set(
+18
View File
@@ -45,6 +45,19 @@ struct BoardSettingsSectionTests {
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .unverifiable) == [])
}
@Test("Pro, 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) == [])
// 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])
}
@Test("The sections carry the headers VoiceOver navigates by")
func headersAreNamed() {
// 10-accessibility.md Board settings sheet: "titled and sectioned with headers VoiceOver
@@ -77,6 +90,11 @@ struct BoardSettingsAvailabilityTests {
// denied ancestor check is never distinguishable from a repository actually being there.
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .unverifiable))
// **Pro, 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.
+70
View File
@@ -583,6 +583,37 @@ struct BranchSwitchSequenceTests {
#expect(GitRepository.branchName(at: fixture.root) == "main")
}
/// The same refusal through the **real** pause rather than an injected one: an unreadable
/// repository is a `GitRepositoryPause` like any other, so the switch's existing gate closes on
/// it with nothing added here (06-history-undo.md Rules, the corrupt-`.git` loud failure, ruled
/// 2026-07-31: "the whole git surface paused").
@Test("A switch refuses against a repository the app cannot open, and changes nothing")
@MainActor
func anUnreadableRepositoryRefusesTheSwitch() async throws {
let (fixture, git) = try await makeGitBoard()
defer { fixture.tearDown() }
let switcher = try makeSwitcher(git)
#expect(GitBranchOperation.createAndSwitch("redesign", at: fixture.root) == .switched("redesign"))
#expect(GitBranchOperation.checkout("main", at: fixture.root) == .switched("main"))
var asked = 0
switcher.settleSessions = { asked += 1; return .proceed }
let head = fixture.root.appendingPathComponent(".git/HEAD")
let savedHead = try Data(contentsOf: head)
try FileManager.default.removeItem(at: head)
await git.committer?.refreshPause()
#expect(git.isRepositoryUnreadable)
#expect(await switcher.switchTo("redesign") == false)
#expect(asked == 0, "no modal, no stamp, no checkout — nothing is attempted at all")
#expect(switcher.lastFailure == nil, "a pause is not a failure; the standing banner is the message")
// The repository is exactly as it was found, which is the promise the row makes.
try savedHead.write(to: head)
#expect(GitRepository.branchName(at: fixture.root) == "main")
}
/// "Contention outlasting the brief retry surfaces as a *waiting* state in the operation's
/// in-progress banner row a wait that persists implausibly long names the lock path never an
/// error dialog, never a hammer."
@@ -946,6 +977,45 @@ struct BoardGitBranchSurfaceTests {
).controlsEnabled)
}
/// **The corrupt-`.git` loud failure at this one control** (06-history-undo.md Rules, ruled
/// 2026-07-31: "Never a silent placeholder discovered only in the popover"). The bug this pins
/// against is the shipped one: `GitRepository.branchName` answers `nil` on a repository it cannot
/// open, and the placeholder meant "still reading" forever.
@Test("An unreadable repository reads as broken, never as still loading")
func anUnreadableSurface() {
let surface = BoardGitBranchSurface.resolve(
branch: nil,
pause: .unreadable,
isSwitching: false,
isWritable: true
)
#expect(surface.branchLabel == BoardGitBranchSurface.unavailableLabel)
#expect(surface.branchLabel != BoardGitBranchSurface.placeholder)
#expect(!surface.isReadingBranch, "there is nothing in flight to be waiting for")
#expect(surface.isRepositoryUnreadable)
#expect(!surface.controlsEnabled, "the whole git surface is paused")
#expect(surface.accessibilityLabel == "Branch unavailable")
// The section's own sentence a sibling of the popover's nested and unverifiable notes, and
// not the pause note, whose second line would be advice about an operation nobody started.
#expect(BoardGitBranchSurface.unreadableNote
== "Lanework can't read this board's git repository, so history is paused; the repository is left untouched.")
}
@Test("A branch name read before the repository broke does not survive the pause")
func anUnreadableSurfaceDropsAStaleBranch() {
let surface = BoardGitBranchSurface.resolve(
branch: "main",
pause: .unreadable,
isSwitching: false,
isWritable: true
)
#expect(surface.branchLabel == BoardGitBranchSurface.unavailableLabel,
"the line must not go on naming a branch nothing can read")
}
@Test("Before the first read the line is a placeholder, not a guess at a branch name")
func theReadingSurface() {
let surface = BoardGitBranchSurface.resolve(
+166
View File
@@ -68,6 +68,37 @@ private func snapshotGitDirectory(_ root: URL) throws -> [SubtreeEntry] {
return entries.sorted { $0.path < $1.path }
}
/// **A `.git` file aimed at nothing** the worktree/submodule pointer shape (`gitdir: `), which
/// detection reads as a repository (presence is presence, 06 Rules Detection) and libgit2 cannot
/// open, because the directory it names is not there.
private func plantDanglingGitPointer(in fixture: WriterFixture) throws {
let target = fixture.root.appendingPathComponent("nowhere/.git/worktrees/board").path
try fixture.file(".git", Data("gitdir: \(target)\n".utf8))
}
/// **A SHA-256 repository, by hand** the layout libgit2 validates, plus the two config keys
/// `git init --object-format=sha256` writes (06 Repository hygiene: "SHA-256 repositories are
/// unsupported, safely an adopted SHA-256 repo the engine cannot open takes the corrupt-repo
/// loud-failure path").
///
/// Built by hand rather than by `git init --object-format=sha256` for the file's standing reason:
/// there is no `/usr/bin/git` in this feature's promise, so there is none in its tests. What makes
/// the fixture honest is that nothing here is a mock the bytes are the ones git writes, and the
/// refusal is libgit2's own.
private func plantSHA256Repository(in fixture: WriterFixture) throws {
try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
try fixture.file(".git/objects/info/.keep", Data())
try fixture.file(".git/refs/heads/.keep", Data())
try fixture.file(".git/config", Data("""
[core]
\trepositoryformatversion = 1
\tbare = false
[extensions]
\tobjectformat = sha256
""".utf8))
}
// MARK: - Composition
@MainActor
@@ -157,6 +188,141 @@ struct HistoryStoreCompositionTests {
}
}
// MARK: - The unreadable repository
/// **A `.git` that isn't a valid repository still reads as git mode and fails loudly**
/// (06-history-undo.md Rules, ruled 2026-07-31).
///
/// The probe is `GitRepository.canOpen(at:)` the same `Repository.open` every read in that file
/// makes run at composition, seeding the committer's pause so the whole git surface is held from
/// the first moment rather than from the first debounce. Every fixture here is a real shape from the
/// wild: a half-made `.git`, a worktree pointer aimed at nothing, and a SHA-256 repository this
/// engine has no support for.
@MainActor
@Suite("HistoryStore ▸ the unreadable repository")
struct HistoryStoreUnreadableRepositoryTests {
@Test("A repository that opens reads readable, and holds nothing")
func aValidRepositoryIsReadable() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#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))
#expect(git.mode == .git)
#expect(!git.isRepositoryUnreadable)
#expect(git.committer?.pause == nil)
#expect(GitRepository.canOpen(at: fixture.root))
}
@Test("A corrupt `.git` stays git mode, reads unreadable, and holds the surface from the first moment")
func aCorruptGitDirectoryIsUnreadable() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
// A `.git` with nothing in it but a plausible HEAD: enough for detection, which asks the
// 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))
// **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
// against an existing `.git` ("init into a repairable repo is exactly the never-mutate
// hazard").
#expect(git.mode == .git)
#expect(git.isRepositoryUnreadable)
#expect(!GitRepository.canOpen(at: fixture.root))
// The pause is seeded at *detection*, before anything has been attempted: the surface is
// held and the banner is raised at the open rather than a debounce later.
#expect(git.committer?.pause == .unreadable)
#expect(git.committer?.lastFailure == nil, "a pause is not a failure")
// And the one operation that could make it worse is refused, whatever the mode read.
#expect(await git.addGit() == false)
}
@Test("A worktree pointer aimed at nothing reads unreadable — the file shape, not just the directory one")
func aDanglingPointerIsUnreadable() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantDanglingGitPointer(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#expect(git.mode == .git, "a `.git` file is a repository to git — presence is presence")
#expect(git.isRepositoryUnreadable)
}
@Test("A SHA-256 repository takes the same path, by construction")
func aSHA256RepositoryIsUnreadable() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantSHA256Repository(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#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".
#expect(git.isRepositoryUnreadable)
}
@Test("Probing an unreadable repository touches nothing")
func theProbeIsARead() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let before = try snapshotGitDirectory(fixture.root)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#expect(git.isRepositoryUnreadable)
// "Lanework leaves the repository untouched" the same bytes and the same mtimes, on the
// one path where a repair instinct would be most tempting.
#expect(try snapshotGitDirectory(fixture.root) == before)
}
@Test("The identity write is refused against a repository the app cannot open")
func identityWritesAreRefused() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
await git.writeIdentity(name: "Ada", email: "[email protected]")
#expect(!fixture.exists(".git/config"), "no config was written into a repository nothing can open")
#expect(git.identityFailure == nil, "and nothing was attempted, so there is nothing to report")
}
/// The seam every git operation consults before it runs (`GitCommitOperation.reading`), asked
/// directly: one word is what holds the auto-commit flush, skips housekeeping, disables Undo/Redo
/// and the branch controls, and defers the interrupted-operation recovery.
@Test("The repository reading reports the pause every operation gates on")
func theReadingReportsThePause() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let reading = GitCommitOperation.reading(at: fixture.root)
#expect(reading.pause == .unreadable)
#expect(!reading.isUnborn)
#expect(!reading.isIndexLocked)
// Optional work simply does not happen (06 Repository hygiene: skipped under a pause).
#expect(GitHousekeeping.run(at: fixture.root, threshold: 1) == .skipped(.held))
// And the app never aborts its own leftover against a repository it cannot open the stamp
// is kept, not cleared, so the leftover stays recognizable as this app's.
let stamp = GitOperationStamp(fromBranch: "main", toBranch: "redesign", headOID: nil)
#expect(GitOperationRecovery.decide(stamp: stamp, pause: .unreadable) == .nothingToDo)
#expect(GitOperationRecovery.decide(stamp: stamp, pause: .merge) == .abort(stamp),
"every other pause still means the app's own leftover")
}
}
// MARK: - Add git
@MainActor