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:
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Kanban
|
||||
|
||||
/// A window ref is two things at once — the value `WindowGroup(for:)` keys its windows on, and the
|
||||
/// argument the host is built from — so it has exactly two promises: it survives the round trip
|
||||
/// through a scene's archive, and two refs naming one thing are *equal*. Both are pinned here,
|
||||
/// because a failure of either is invisible until the app opens a second window for something that
|
||||
/// already has one.
|
||||
|
||||
@Suite("Window refs")
|
||||
struct WindowRefsTests {
|
||||
|
||||
// MARK: Codable
|
||||
|
||||
@Test("Both refs round-trip through JSON unchanged")
|
||||
func refsRoundTripThroughCoding() throws {
|
||||
let board = BoardWindowRef(path: "/Users/x/Boards/Work.kanban")
|
||||
let card = CardWindowRef(boardPath: board.path, cardID: "55555555-5555-4555-8555-555555555555")
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
#expect(try decoder.decode(BoardWindowRef.self, from: encoder.encode(board)) == board)
|
||||
|
||||
let decodedCard = try decoder.decode(CardWindowRef.self, from: encoder.encode(card))
|
||||
#expect(decodedCard == card)
|
||||
#expect(decodedCard.cardID == card.cardID, "the folder's exact spelling survives, not just its value")
|
||||
#expect(decodedCard.boardPath == board.path)
|
||||
}
|
||||
|
||||
@Test("A board ref and a URL agree in both directions")
|
||||
func boardRefAndURLAgree() {
|
||||
let url = URL(fileURLWithPath: "/Users/x/Boards/Work.kanban", isDirectory: true)
|
||||
let ref = BoardWindowRef(url: url)
|
||||
#expect(ref.path == url.path)
|
||||
#expect(ref.url.path == url.path)
|
||||
}
|
||||
|
||||
// MARK: Identity
|
||||
|
||||
@Test("A card ref's id compares by UUID value, not by spelling")
|
||||
func cardRefFoldsTheCaseOfItsCardID() {
|
||||
let board = "/Users/x/Boards/Work.kanban"
|
||||
let lower = CardWindowRef(boardPath: board, cardID: "55555555-5555-4555-8555-555555555555")
|
||||
let upper = CardWindowRef(boardPath: board, cardID: "55555555-5555-4555-8555-555555555555".uppercased())
|
||||
|
||||
// The consequence that matters: `WindowGroup(for:)` keys on this equality, so a card whose
|
||||
// folder is spelled in caps focuses the window the lowercase spelling already opened instead
|
||||
// of opening a second one for the same card.
|
||||
#expect(lower == upper)
|
||||
#expect(lower.hashValue == upper.hashValue)
|
||||
#expect(Set([lower, upper]).count == 1)
|
||||
|
||||
// And `rawValue` is untouched by any of that — it is what builds URLs.
|
||||
#expect(upper.cardID == "55555555-5555-4555-8555-555555555555".uppercased())
|
||||
#expect(upper.cardIdentity == lower.cardIdentity)
|
||||
}
|
||||
|
||||
@Test("The board half of a card ref is compared verbatim")
|
||||
func cardRefsOnDifferentBoardsAreDifferent() {
|
||||
let card = "55555555-5555-4555-8555-555555555555"
|
||||
let here = CardWindowRef(boardPath: "/Users/x/Boards/Work.kanban", cardID: card)
|
||||
let there = CardWindowRef(boardPath: "/Users/x/Boards/Home.kanban", cardID: card)
|
||||
|
||||
// The cross-board move rule in one assertion: the card's UUID travels with it, but the key
|
||||
// that named its window does not — so the window dismisses exactly like a delete.
|
||||
#expect(here != there)
|
||||
#expect(here.board == BoardWindowRef(path: "/Users/x/Boards/Work.kanban"))
|
||||
#expect(here.boardURL.path == "/Users/x/Boards/Work.kanban")
|
||||
}
|
||||
|
||||
@Test("A card ref built from an ItemID keeps the id's exact spelling")
|
||||
func cardRefFromItemIDKeepsRawValue() {
|
||||
let board = BoardWindowRef(path: "/Users/x/Boards/Work.kanban")
|
||||
let id = ItemID(rawValue: "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA")
|
||||
let ref = CardWindowRef(board: board, cardID: id)
|
||||
|
||||
#expect(ref.cardID == id.rawValue)
|
||||
#expect(ref == CardWindowRef(boardPath: board.path, cardID: id.rawValue.lowercased()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user