Give the registry live display-state write-through and record-before-load
BoardRecord carries icon/iconColor; recordOpen/recordClose stamp them with the display name, and a displayStateDelegate on the store (wired in BoardWindowHost beside onFrameChanged) syncs all three through BoardRegistry.syncDisplayState on every successful reload — welcome rows now wear the board's own icon and follow in-app renames live. recordOpen now runs before the load with the folder name as a brand-new record's provisional display name, so a first open that fails fail-fast still lands in recents carrying the failure row-level (02's rule); an existing record's cached name survives a failing retry, and the welcome fallback list remains only for failures naming no record at all. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -54,14 +54,15 @@ private func makeModel() throws -> (model: AppModel, tearDown: () -> Void) {
|
||||
return (model, { try? FileManager.default.removeItem(at: folder) })
|
||||
}
|
||||
|
||||
/// Opens a board the way `BoardWindowHost` does — acquire, record, flag, begin — so the close tests
|
||||
/// are closing something the app would recognise.
|
||||
/// Opens a board the way `BoardWindowHost` does — record, acquire, name it for real, flag, begin —
|
||||
/// so the close tests are closing something the app would recognise.
|
||||
@MainActor
|
||||
@discardableResult
|
||||
private func openBoard(_ model: AppModel, at url: URL) throws -> BoardWindowRef {
|
||||
let ref = BoardWindowRef(url: url)
|
||||
let recordID = model.boardRegistry.recordOpen(of: url)
|
||||
let store = try model.storeRegistry.acquire(url)
|
||||
let recordID = model.boardRegistry.recordOpen(of: url, displayName: AppModel.displayName(of: store))
|
||||
model.boardRegistry.syncDisplayState(id: recordID, title: AppModel.displayName(of: store), icon: nil, iconColor: nil)
|
||||
model.boardRegistry.setOpenNow(id: recordID)
|
||||
model.beginSession(ref: ref, store: store, recordID: recordID, access: nil)
|
||||
return ref
|
||||
@@ -141,6 +142,84 @@ struct AppModelTests {
|
||||
#expect(AppModel.displayName(of: untitled) == "Untitled Board", "sans extension, per 01 § Board naming")
|
||||
}
|
||||
|
||||
// MARK: Record before load
|
||||
|
||||
/// `BoardWindowHost.start()`'s own sequence for a board whose load fails fail-fast: record
|
||||
/// first, then attempt the load, then (on failure) file the launch failure and refresh the
|
||||
/// cached recents this test reads back through `WelcomeRow.derive`, exactly as welcome would.
|
||||
@Test("A first open that fails fail-fast still records — a folder-name row carrying the failure")
|
||||
func failFastOpenStillRecordsAndSurfacesRowLevel() async throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
// The board root's own `index.md` fails to parse — fail-fast's own case, thrown the moment
|
||||
// `BoardStore.init` (behind `storeRegistry.acquire`) walks it.
|
||||
try fixture.item("", "---\nschema: 1\nlabels: [a, b\n---\nbody\n")
|
||||
let (model, tearDown) = try makeModel()
|
||||
defer { tearDown() }
|
||||
|
||||
let ref = BoardWindowRef(url: fixture.root)
|
||||
let recordID = model.boardRegistry.recordOpen(of: fixture.root)
|
||||
|
||||
var failureMessage: String?
|
||||
do throws(BoardLoadError) {
|
||||
_ = try model.storeRegistry.acquire(fixture.root)
|
||||
Issue.record("expected the load to fail fail-fast")
|
||||
} catch {
|
||||
failureMessage = error.description
|
||||
model.recordLaunchFailure(path: ref.path, message: error.description)
|
||||
}
|
||||
model.refreshRecents()
|
||||
|
||||
// The record exists, provisionally named after the folder — nothing loaded, so nothing else
|
||||
// was there to trust (02 § Per-board app state).
|
||||
let record = try #require(model.boardRegistry.record(id: recordID))
|
||||
#expect(record.displayName == fixture.root.deletingPathExtension().lastPathComponent)
|
||||
#expect(record.laneCount == nil, "never opened successfully, so nothing was ever counted")
|
||||
|
||||
// And the failure lands on that very row — uniform with a failed restoration's row, never
|
||||
// the separate list of failures naming no record.
|
||||
let derived = WelcomeRow.derive(recents: model.recents, failures: model.launchFailures)
|
||||
let row = try #require(derived.rows.first { $0.id == recordID })
|
||||
guard case let .failed(message) = row.caption else {
|
||||
Issue.record("expected the row to carry the failure, got \(row.caption)")
|
||||
return
|
||||
}
|
||||
#expect(message == failureMessage)
|
||||
#expect(derived.unmatched.isEmpty, "the failure landed on a row it already had, not the fallback list")
|
||||
}
|
||||
|
||||
@Test("Retrying a fail-fast open reuses the same record, and a later success replaces its name")
|
||||
func retryingAFailFastOpenReusesTheRecord() async throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
let brokenIndex = "---\nschema: 1\nlabels: [a, b\n---\nbody\n"
|
||||
try fixture.item("", brokenIndex)
|
||||
let (model, tearDown) = try makeModel()
|
||||
defer { tearDown() }
|
||||
|
||||
let ref = BoardWindowRef(url: fixture.root)
|
||||
let firstAttempt = model.boardRegistry.recordOpen(of: fixture.root)
|
||||
do throws(BoardLoadError) {
|
||||
_ = try model.storeRegistry.acquire(fixture.root)
|
||||
Issue.record("expected the load to fail fail-fast")
|
||||
} catch {
|
||||
model.recordLaunchFailure(path: ref.path, message: error.description)
|
||||
}
|
||||
model.refreshRecents()
|
||||
#expect(model.boardRegistry.recents().count == 1, "a retry updates the one record, it does not add one")
|
||||
|
||||
// The user fixes the file and retries — `BoardWindowHost.start()`'s exact sequence again.
|
||||
try fixture.item("", "---\nschema: 1\ntitle: Fixed Board\n---\nbody\n")
|
||||
let secondAttempt = model.boardRegistry.recordOpen(of: fixture.root)
|
||||
#expect(secondAttempt == firstAttempt, "the same folder is the same board, failed or not")
|
||||
|
||||
let store = try model.storeRegistry.acquire(fixture.root)
|
||||
model.boardRegistry.syncDisplayState(id: secondAttempt, title: AppModel.displayName(of: store), icon: nil, iconColor: nil)
|
||||
|
||||
let record = try #require(model.boardRegistry.record(id: secondAttempt))
|
||||
#expect(record.displayName == "Fixed Board", "the first success replaces the provisional folder name")
|
||||
}
|
||||
|
||||
// MARK: Sessions
|
||||
|
||||
@Test("A user close stamps live counts, unflags the board, and lets the store go")
|
||||
|
||||
Reference in New Issue
Block a user