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:
2026-07-27 20:44:30 -04:00
parent cacd48cb0f
commit f2d9f3ad07
10 changed files with 608 additions and 41 deletions
+198 -5
View File
@@ -121,6 +121,51 @@ struct BoardRegistryTests {
#expect(registry.record(id: id)?.lastKnownPath == renamed.path)
}
@Test("Recording an open with no displayName gives a brand-new record the folder name")
func recordOpenWithNoDisplayNameUsesTheFolderName() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let fixture = try makeBoard()
defer { fixture.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
// The "record before load" call (02-architecture.md § Per-board app state): nothing
// authoritative is known yet, so the provisional name is the folder's own "the folder
// name is the Finder document name the user just picked."
let id = registry.recordOpen(of: fixture.root)
let record = try #require(registry.record(id: id))
#expect(record.displayName == fixture.root.deletingPathExtension().lastPathComponent)
#expect(!record.bookmark.isEmpty, "the bookmark still mints on the before-load call")
#expect(record.icon == nil)
#expect(record.iconColor == nil)
}
@Test("Recording an open with no displayName never regresses an existing record's cached name")
func recordOpenWithNoDisplayNamePreservesAnExistingRecord() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let fixture = try makeBoard()
defer { fixture.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
// A first, successful-looking open: real values, as a caller with a loaded snapshot passes.
let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board", icon: "star", iconColor: "fern")
let firstOpened = try #require(registry.record(id: id)).lastOpened
try await Task.sleep(for: .milliseconds(5))
// A later open attempt whose load has not run yet or never gets the chance to, because it
// fails fail-fast. Either way, this call alone must not know that, so the cached title and
// style survive untouched; only the bits every open refreshes regardless move.
let again = registry.recordOpen(of: fixture.root)
#expect(again == id)
let record = try #require(registry.record(id: id))
#expect(record.displayName == "Todo Board", "the last successful open's title is still the best information this row has")
#expect(record.icon == "star")
#expect(record.iconColor == "fern")
#expect(record.lastOpened > firstOpened, "the bookmark and lastOpened still refresh on every open attempt")
#expect(!record.bookmark.isEmpty)
}
// MARK: Counts
@Test("Counts are stamped at close and read back without a scan")
@@ -132,7 +177,7 @@ struct BoardRegistryTests {
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board")
registry.recordClose(id: id, laneCount: 2, cardCount: 7)
registry.recordClose(id: id, displayName: "Todo Board", laneCount: 2, cardCount: 7)
// The board grows after the close an agent filing cards, a colleague's pull. A welcome
// window that scanned would notice; this one must not, because scanning is what makes
@@ -152,6 +197,154 @@ struct BoardRegistryTests {
#expect(FileIdentity(of: url) == FileIdentity(of: fixture.root))
}
// MARK: Title, icon, and iconColor
@Test("Icon and iconColor are stamped at open and re-stamped at close, alongside the title")
func openAndCloseStampIconAndIconColor() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let fixture = try makeBoard()
defer { fixture.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board", icon: "star", iconColor: "fern")
let opened = try #require(registry.record(id: id))
#expect(opened.icon == "star")
#expect(opened.iconColor == "fern")
// The board is restyled while open an in-app change or a foreign edit, it makes no
// difference here since `recordClose` re-stamps from whatever the store last held.
registry.recordClose(
id: id,
displayName: "Todo Board",
laneCount: 2,
cardCount: 7,
icon: "heart",
iconColor: "carnation"
)
let closed = try #require(registry.record(id: id))
#expect(closed.icon == "heart")
#expect(closed.iconColor == "carnation")
// A board can also lose its override entirely `nil` closes back over a previously
// stamped value rather than being mistaken for "leave it alone".
registry.recordClose(id: id, displayName: "Todo Board", laneCount: 2, cardCount: 7)
#expect(registry.record(id: id)?.icon == nil)
#expect(registry.record(id: id)?.iconColor == nil)
}
@Test("The live write-through updates title, icon, and iconColor when they differ from the cached record")
func syncDisplayStateWritesThroughOnlyWhatChanged() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let fixture = try makeBoard()
defer { fixture.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board")
#expect(registry.record(id: id)?.icon == nil, "no override yet")
// An in-app rename/restyle's reload lands new values the seam `BoardStore`'s
// `displayStateDelegate` calls into.
registry.syncDisplayState(id: id, title: "Renamed", icon: "star.fill", iconColor: "deep-sky-blue")
let afterFirstSync = try #require(registry.record(id: id))
#expect(afterFirstSync.displayName == "Renamed")
#expect(afterFirstSync.icon == "star.fill")
#expect(afterFirstSync.iconColor == "deep-sky-blue")
// A later reload whose display state is unchanged from the cached record leaves it exactly
// as it was the no-op-skip rule every other setter in this file already keeps.
registry.syncDisplayState(id: id, title: "Renamed", icon: "star.fill", iconColor: "deep-sky-blue")
#expect(registry.record(id: id) == afterFirstSync)
// The values persist like any other mutation here.
let reloaded = BoardRegistry(storageURL: storage.url)
#expect(reloaded.record(id: id)?.displayName == "Renamed")
#expect(reloaded.record(id: id)?.icon == "star.fill")
#expect(reloaded.record(id: id)?.iconColor == "deep-sky-blue")
}
@Test("The live write-through on an id with no record is a no-op, not a crash")
func syncDisplayStateOnUnknownIDIsANoOp() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
registry.syncDisplayState(id: UUID(), title: "Ghost", icon: "star", iconColor: "fern")
#expect(registry.recents().isEmpty, "a window that outlived its record must not resurrect one")
}
@Test("A registry file written before the icon/iconColor keys existed still decodes, with both nil")
func oldRegistryFilesDecodeWithoutIconKeys() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
// Byte-for-byte the shape this file had before icon/iconColor existed the same evolution
// rule `oldRegistryFilesDecodeWithoutTheOpenNowKey` pins for `isOpenNow`: a key added here
// must be optional, or every existing user's recents empties on upgrade.
let id = UUID()
let garbage = Data("not a bookmark".utf8).base64EncodedString()
let json = """
[
{
"bookmark" : "\(garbage)",
"cardCount" : 9,
"displayName" : "Archive",
"id" : "\(id.uuidString)",
"laneCount" : 4,
"lastKnownPath" : "/Volumes/Archive/Boards/Archive",
"lastOpened" : "2026-01-01T09:00:00.000Z",
"pushOnCommit" : true,
"remoteLocationWarned" : true
}
]
"""
try Data(json.utf8).write(to: storage.url)
let registry = BoardRegistry(storageURL: storage.url)
#expect(registry.recents().count == 1, "the file decoded; nothing was quarantined")
#expect(registry.record(id: id)?.icon == nil, "a missing key reads as 'no override'")
#expect(registry.record(id: id)?.iconColor == nil)
// And both keys write through from here on.
registry.syncDisplayState(id: id, title: "Archive", icon: "archivebox", iconColor: "aluminum")
let updated = BoardRegistry(storageURL: storage.url).record(id: id)
#expect(updated?.icon == "archivebox")
#expect(updated?.iconColor == "aluminum")
}
@Test("A registry file with icon and iconColor present decodes them")
func registryFileWithIconKeysDecodes() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let id = UUID()
let garbage = Data("not a bookmark".utf8).base64EncodedString()
let json = """
[
{
"bookmark" : "\(garbage)",
"cardCount" : 9,
"displayName" : "Archive",
"icon" : "archivebox",
"iconColor" : "aluminum",
"id" : "\(id.uuidString)",
"laneCount" : 4,
"lastKnownPath" : "/Volumes/Archive/Boards/Archive",
"lastOpened" : "2026-01-01T09:00:00.000Z",
"pushOnCommit" : true,
"remoteLocationWarned" : true
}
]
"""
try Data(json.utf8).write(to: storage.url)
let registry = BoardRegistry(storageURL: storage.url)
#expect(registry.record(id: id)?.icon == "archivebox")
#expect(registry.record(id: id)?.iconColor == "aluminum")
}
// MARK: Orphaning
@Test("A deleted board is orphaned in recents and can be forgotten")
@@ -162,7 +355,7 @@ struct BoardRegistryTests {
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Doomed")
registry.recordClose(id: id, laneCount: 1, cardCount: 1)
registry.recordClose(id: id, displayName: "Doomed", laneCount: 1, cardCount: 1)
fixture.tearDown()
let rows = registry.recents()
@@ -311,7 +504,7 @@ struct BoardRegistryTests {
let firstID = registry.recordOpen(of: first.root, displayName: "First")
try await Task.sleep(for: .milliseconds(5))
let secondID = registry.recordOpen(of: second.root, displayName: "Second")
registry.recordClose(id: firstID, laneCount: 3, cardCount: 11)
registry.recordClose(id: firstID, displayName: "First", laneCount: 3, cardCount: 11)
registry.updateWindowFrame(id: firstID, frame: WindowFrame(x: 120, y: 60, width: 1440, height: 900))
registry.setPushOnCommit(id: firstID, true)
registry.setRemoteLocationWarned(id: firstID)
@@ -377,7 +570,7 @@ struct BoardRegistryTests {
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Untouched")
registry.recordClose(id: id, laneCount: 1, cardCount: 1)
registry.recordClose(id: id, displayName: "Untouched", laneCount: 1, cardCount: 1)
registry.updateWindowFrame(id: id, frame: WindowFrame(x: 0, y: 0, width: 800, height: 600))
registry.setPushOnCommit(id: id, true)
registry.setRemoteLocationWarned(id: id)
@@ -443,7 +636,7 @@ struct BoardRegistryTests {
// whole restoration mechanism, so it is asserted rather than assumed, and asserted across a
// reload of the file because a relaunch is what consumes it.
registry.setOpenNow(id: id)
registry.recordClose(id: id, laneCount: 2, cardCount: 5)
registry.recordClose(id: id, displayName: "Work", laneCount: 2, cardCount: 5)
let afterRelaunch = BoardRegistry(storageURL: storage.url)
#expect(afterRelaunch.record(id: id)?.isOpenNow == true, "the flags describe what was open at quit")