Stand up the window architecture — welcome, board, card
Four scenes (welcome, restore bootstrap, board group, card group) with system restoration disabled in favor of the registry's open-now flags: set when a window actually opens, cleared only on user close, so quit — and crash — leave exactly the restoration set behind. AppModel joins windows to sessions (shared store, registry record, card refs, held security scope); CloseFlushCoordinator pins 02's strict close order as a seam-injected machine (card sessions end, windows drain, store flushes, record stamps, teardown) with named slots where m6/m7 flushes land. HostedWindowController proxies — never replaces — SwiftUI's window delegate to intercept windowShouldClose for the flush, report frames, and place saved frames onto live screens. Card windows are (board path, case-folded card id) values: reopen focuses, and a snapshot-pure fate function dismisses on delete, tombstone, tombstoned lane, or cross-board move. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -346,6 +346,118 @@ struct BoardRegistryTests {
|
||||
#expect(ids(registry.recents()) == [firstID, thirdID, secondID])
|
||||
}
|
||||
|
||||
// MARK: The open-now marker
|
||||
|
||||
@Test("Opening flags a board, a user close unflags it, and quit deliberately leaves it standing")
|
||||
func openNowTracksWindowsAndSurvivesQuit() 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: "Work")
|
||||
#expect(registry.record(id: id)?.isOpenNow == nil, "recording an open is not opening a window")
|
||||
#expect(registry.restorables().isEmpty)
|
||||
|
||||
registry.setOpenNow(id: id)
|
||||
#expect(registry.record(id: id)?.isOpenNow == true)
|
||||
#expect(ids(registry.restorables()) == [id])
|
||||
|
||||
// A user close. The flag goes, and with it the board's place in the next launch.
|
||||
registry.clearOpenNow(id: id)
|
||||
#expect(registry.record(id: id)?.isOpenNow == false)
|
||||
#expect(registry.restorables().isEmpty)
|
||||
|
||||
// A quit. The teardown stamps counts and does *not* clear the flag — that omission is the
|
||||
// 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)
|
||||
|
||||
let afterRelaunch = BoardRegistry(storageURL: storage.url)
|
||||
#expect(afterRelaunch.record(id: id)?.isOpenNow == true, "the flags describe what was open at quit")
|
||||
#expect(ids(afterRelaunch.restorables()) == [id])
|
||||
#expect(afterRelaunch.record(id: id)?.laneCount == 2)
|
||||
}
|
||||
|
||||
@Test("Restorables are the flagged records only, oldest first")
|
||||
func restorablesReopenInLastOpenedOrder() async throws {
|
||||
let storage = try RegistryStorage()
|
||||
defer { storage.tearDown() }
|
||||
let first = try makeBoard()
|
||||
defer { first.tearDown() }
|
||||
let second = try makeBoard()
|
||||
defer { second.tearDown() }
|
||||
let third = try makeBoard()
|
||||
defer { third.tearDown() }
|
||||
let registry = BoardRegistry(storageURL: storage.url)
|
||||
|
||||
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")
|
||||
try await Task.sleep(for: .milliseconds(5))
|
||||
let thirdID = registry.recordOpen(of: third.root, displayName: "Third")
|
||||
|
||||
registry.setOpenNow(id: firstID)
|
||||
registry.setOpenNow(id: thirdID)
|
||||
|
||||
// Ascending, the exact inverse of `recents()` — these are reopened in order, so the board
|
||||
// opened last at quit opens last again and ends up frontmost.
|
||||
#expect(ids(registry.restorables()) == [firstID, thirdID])
|
||||
#expect(ids(registry.recents()) == [thirdID, secondID, firstID], "recents is unchanged, and still newest first")
|
||||
#expect(!ids(registry.restorables()).contains(secondID), "a board that was not open does not restore")
|
||||
|
||||
// A flagged board whose folder has gone still comes back — classified, not dropped. The
|
||||
// launch flow renders it as a failed restoration rather than pretending it was never open.
|
||||
third.tearDown()
|
||||
let rows = registry.restorables()
|
||||
#expect(ids(rows) == [firstID, thirdID])
|
||||
guard case .unavailable = rows[1] else {
|
||||
Issue.record("expected the deleted board to classify unavailable, got \(rows[1])")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A registry file written before the open-now key existed still decodes")
|
||||
func oldRegistryFilesDecodeWithoutTheOpenNowKey() async throws {
|
||||
let storage = try RegistryStorage()
|
||||
defer { storage.tearDown() }
|
||||
|
||||
// Byte-for-byte the shape this file had one milestone ago: no `isOpenNow` anywhere. The
|
||||
// struct's evolution rule says a new key must be optional precisely so this file survives —
|
||||
// a required key would have failed to decode, quarantined the file, and emptied the user's
|
||||
// recents 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)?.isOpenNow == nil, "a missing key reads as 'not open'")
|
||||
#expect(registry.restorables().isEmpty)
|
||||
#expect(registry.record(id: id)?.laneCount == 4, "and every other field survived")
|
||||
|
||||
// And the key writes through from here on.
|
||||
registry.setOpenNow(id: id)
|
||||
#expect(BoardRegistry(storageURL: storage.url).record(id: id)?.isOpenNow == true)
|
||||
}
|
||||
|
||||
// MARK: Bookmarks in a sandboxed host
|
||||
|
||||
@Test("A bookmark is always produced, and resolves back to the same folder")
|
||||
|
||||
Reference in New Issue
Block a user