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