import Foundation import Testing @testable import Kanban /// The two inline editors' **write** paths — `BoardStore.commitRename` and /// `BoardStore.commitPlaceholder` — plus the lane drag's `moveLane`. /// /// Like `LaneWidthWriteTests`, these drive a real store over a real temp board and then read the /// **raw bytes** back rather than the app's own read path: the interesting claims are about the /// file — the key that lands or leaves, the stamps that follow, and everything else surviving /// byte-for-byte. `WriterFixture`, `Ident` and `Item` come from `WriterTestSupport.swift`. // MARK: - Fixtures private func tombstoned(order: String, title: String) -> String { """ --- schema: 1 title: \(title) order: \(order) deleted: 2026-03-03T09:00:00Z --- \(title) body. """ } /// An item with no `title` key at all — the untitled state a rename can both start from and return /// an item to. private func untitled(order: String) -> String { """ --- schema: 1 order: \(order) project: lanework # agent overlay created: 2026-01-01T09:00:00Z modified: 2026-02-02T09:00:00Z --- Some body. """ } /// Two lanes: `lane1` with two cards, `lane2` with one, plus a readable-but-uneditable lane. @MainActor private func makeBoard() throws -> WriterFixture { let fixture = try WriterFixture() try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo")) try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First")) try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing")) try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Third")) try fixture.item(Ident.lane3, Item.uneditable) return fixture } private let lane1 = ItemID(rawValue: Ident.lane1) private let lane2 = ItemID(rawValue: Ident.lane2) private let lane3 = ItemID(rawValue: Ident.lane3) private let card1 = ItemID(rawValue: Ident.card1) private let card2 = ItemID(rawValue: Ident.card2) private let card3 = ItemID(rawValue: Ident.card3) /// The file's lines minus the ones an app-mediated write is *supposed* to change — what must come /// through a rename byte-for-byte, in order. private func untouchedLines(_ text: String) -> [Substring] { text.split(separator: "\n", omittingEmptySubsequences: false).filter { !$0.hasPrefix("modified") && !$0.hasPrefix("title:") } } private func load(_ fixture: WriterFixture) throws -> BoardModel { try BoardLoader.load(boardRoot: fixture.root).model } private func card(_ id: ItemID, in model: BoardModel) -> Card? { model.lanes.flatMap(\.cards).first { $0.id == id } } /// Every card folder under a lane, so a create can be spotted by what is newly there. private func cardFolders(_ lane: String, in fixture: WriterFixture) throws -> Set { Set(try fixture.entryNames(lane).filter { BoardLoader.isUUIDShaped($0) }) } // MARK: - Rename @MainActor @Suite("BoardStore ▸ inline rename") struct InlineRenameWriteTests { @Test("A non-empty commit writes the title, stamps modified, and touches nothing else") func writesTheTitleAndStamps() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)") store.transient.beginRename(of: card1, currentTitle: "First") store.transient.updateRenameDraft("Fix login") store.commitRename() let after = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)") #expect(after.contains("title: Fix login")) #expect(!after.contains("modified-by"), "an app-mediated write clears an external writer's attribution") #expect(!after.contains("modified: 2026-02-02T09:00:00Z"), "the stamp is fresh") // Everything the write does not own survives exactly, in order: the unknown key with its // inline comment, the reserved `labels`, the original `created`, the `order`, and the body. #expect(untouchedLines(after) == untouchedLines(before)) let renamed = try #require(card(card1, in: load(fixture))) #expect(renamed.title == .valid("Fix login")) #expect(renamed.modifiedBy.isMissing) let modified = try #require(renamed.modified.value) #expect(abs(modified.timeIntervalSinceNow) < 60) #expect(store.transient.renameEditor == nil, "the editor closes on commit") #expect(store.banners.oneShots.isEmpty) } @Test("A lane renames through the same path") func renamesALane() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginRename(of: lane2, currentTitle: "Doing") store.transient.updateRenameDraft("In progress") store.commitRename() #expect(try fixture.indexText(Ident.lane2).contains("title: In progress")) #expect(try load(fixture).lanes.first { $0.id == lane2 }?.title == .valid("In progress")) } @Test("An empty commit removes the title key, byte-faithfully") func emptyCommitRemovesTheKey() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)") store.transient.beginRename(of: card1, currentTitle: "First") store.transient.updateRenameDraft("") store.commitRename() let after = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)") // "Committing an empty rename on an existing item removes its `title` key" — the *key*, not // an empty string: titles are optional at every level, and `title: ""` would be a real (if // blank) title where the face should show the untitled placeholder. #expect(!after.contains("title:")) #expect(!after.contains("title: \"\"")) #expect(untouchedLines(after) == untouchedLines(before), "only the title line and the stamps moved") let stripped = try #require(card(card1, in: load(fixture))) #expect(stripped.title.isMissing) #expect(stripped.order == 1024, "the card keeps its place") } @Test("Whitespace commits as empty — a title of three spaces is a slip, not a name") func whitespaceIsEmpty() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginRename(of: card1, currentTitle: "First") store.transient.updateRenameDraft(" ") store.commitRename() #expect(try #require(card(card1, in: load(fixture))).title.isMissing) // And a title with edges is trimmed rather than quoted into the file with its padding. store.transient.beginRename(of: card2, currentTitle: "Second") store.transient.updateRenameDraft(" Fix login ") store.commitRename() #expect(try #require(card(card2, in: load(fixture))).title == .valid("Fix login")) } @Test("An unchanged title writes nothing at all") func unchangedTitleIsANoOp() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.item("\(Ident.lane2)/\(Ident.card3)", untitled(order: "1024")) let store = try BoardStore(rootURL: fixture.root) let titled = try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") let untouched = try fixture.indexData("\(Ident.lane2)/\(Ident.card3)") // An editor opened and dismissed with Return must not stamp `modified` or (on a git board) // mint a commit — the lane-resize rule, for the same reason. store.transient.beginRename(of: card1, currentTitle: "First") store.commitRename() #expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == titled) // Same for an untitled item committed still untitled: the key must not materialize and then // vanish, nor the file be rewritten to say nothing new. store.transient.beginRename(of: card3, currentTitle: nil) store.commitRename() #expect(try fixture.indexData("\(Ident.lane2)/\(Ident.card3)") == untouched) #expect(try fixture.entryNames("\(Ident.lane2)/\(Ident.card3)") == ["index.md"], "no temp-file residue either") } @Test("A commit at a target the snapshot does not have writes nothing, silently") func vanishedTargetWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.entryNames("") // "A target that is tombstoned, deleted, or gone at commit time discards the editor and its // keystrokes silently" — silently being the operative word: nothing written, no banner. store.transient.beginRename(of: ItemID(rawValue: Ident.indexless), currentTitle: "Ghost") store.transient.updateRenameDraft("Never lands") store.commitRename() #expect(try fixture.entryNames("") == before) #expect(!fixture.exists(Ident.indexless)) #expect(store.transient.renameEditor == nil) #expect(store.banners.oneShots.isEmpty) } @Test("A commit at a card under a tombstoned lane writes nothing — liveness is effective") func targetUnderATombstonedLaneWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } // The lane carries the tombstone; the card's own flag is untouched, and it renders nowhere // regardless (03-board-ui.md collapses the lane to one trash entry). try fixture.item(Ident.lane1, tombstoned(order: "1024", title: "Todo")) let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") store.transient.beginRename(of: card1, currentTitle: "First") store.transient.updateRenameDraft("Never lands") store.commitRename() #expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == before) #expect(store.banners.oneShots.isEmpty) } @Test("A rename follows a foreign move — the write lands wherever the card now lives") func renameFollowsTheUUID() async throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginRename(of: card1, currentTitle: "First") store.transient.updateRenameDraft("Fix login") try FileManager.default.moveItem( at: fixture.url("\(Ident.lane1)/\(Ident.card1)"), to: fixture.url("\(Ident.lane2)/\(Ident.card1)") ) store.handleWatcherEvent(.treeChanged(.foreign)) await store.awaitQuiescence() store.commitRename() #expect(try fixture.indexText("\(Ident.lane2)/\(Ident.card1)").contains("title: Fix login")) #expect(store.banners.oneShots.isEmpty) } @Test("A readable-but-uneditable target refuses the write, banners it, and keeps its bytes") func uneditableTargetBanners() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(Ident.lane3) store.transient.beginRename(of: lane3, currentTitle: "Odd") store.transient.updateRenameDraft("Renamed") store.commitRename() #expect(try fixture.indexData(Ident.lane3) == before) #expect(store.banners.oneShots.count == 1) let posted = try #require(store.banners.oneShots.first) // The title is enriched off the document the write refused, so the banner names the item by // what it is still called rather than by the name that never landed. #expect(posted.error.operation == .rename(title: "Odd")) #expect(BannerCenter.headline(for: posted.error).hasPrefix("Couldn't rename 'Odd' — ")) } @Test("A read-only board refuses the rename without a second banner") func readOnlyBoardRefusesQuietly() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.enterVanishedRootLock() let before = try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") store.transient.beginRename(of: card1, currentTitle: "First") store.transient.updateRenameDraft("Fix login") store.commitRename() #expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == before) #expect(store.banners.oneShots.isEmpty, "the lock row is already standing") #expect(store.bannerRows.contains { $0.id == "read-only-lock" }) } } // MARK: - The new-card placeholder's commit @MainActor @Suite("BoardStore ▸ new-card placeholder commit") struct NewCardCommitWriteTests { @Test("A lane-bottom commit creates the card appended after the visible cards") func createsAtTheLaneBottom() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try cardFolders(Ident.lane1, in: fixture) store.transient.beginPlaceholder(inLane: lane1) store.transient.updateDraft("Fix login") let created = try #require(store.commitPlaceholder()) let after = try cardFolders(Ident.lane1, in: fixture) #expect(after.subtracting(before) == [created.rawValue], "exactly one new card folder") let model = try load(fixture) let made = try #require(card(created, in: model)) #expect(made.title == .valid("Fix login")) // Appended after the visible siblings at 1024 and 2048: `Ranks.append` is max + 1024. #expect(made.order == 3072) #expect(model.lanes.first { $0.id == lane1 }?.cards.map(\.id) == [card1, card2, created], "and it lands last in display order") // The overlay stands in for the card until the watcher round-trips it — it does not vanish // the instant the Writer returns (02-architecture.md § Layering). #expect(store.transient.newCardPlaceholder?.phase == .awaitingArrival(created)) #expect(store.banners.oneShots.isEmpty) } @Test("An after-anchor commit lands the card between its anchor and the next card") func createsAfterItsAnchor() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) // ⌘N with `card1` selected: "in that card's lane, immediately after it". store.transient.beginPlaceholder(inLane: lane1, after: card1) store.transient.updateDraft("Fix login") let created = try #require(store.commitPlaceholder()) let model = try load(fixture) let made = try #require(card(created, in: model)) #expect(made.order == 1536, "the midpoint of 1024 and 2048") #expect(model.lanes.first { $0.id == lane1 }?.cards.map(\.id) == [card1, created, card2]) #expect(made.title == .valid("Fix login")) // The reposition rides the Writer's same-parent degenerate reorder, so exactly one file was // rewritten past the create: the neighbours keep their ranks. #expect(card(card1, in: model)?.order == 1024) #expect(card(card2, in: model)?.order == 2048) } @Test("An anchor that is already last is the lane's bottom — no reposition") func anchoringTheLastCardIsAnAppend() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginPlaceholder(inLane: lane1, after: card2) store.transient.updateDraft("Fix login") let created = try #require(store.commitPlaceholder()) #expect(try #require(card(created, in: load(fixture))).order == 3072) } @Test("An anchor that vanished degrades to the lane's bottom rather than refusing") func aVanishedAnchorAppends() async throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginPlaceholder(inLane: lane1, after: card1) store.transient.updateDraft("Fix login") // An agent deletes the anchor mid-typing. The lane — the anchor that actually matters — is // still there, so the card the user is creating is theirs to keep. try FileManager.default.removeItem(at: fixture.url("\(Ident.lane1)/\(Ident.card1)")) store.handleWatcherEvent(.treeChanged(.foreign)) await store.awaitQuiescence() let created = try #require(store.commitPlaceholder()) #expect(try #require(card(created, in: load(fixture))).order == 3072) } @Test("An empty commit discards the draft and writes nothing") func emptyCommitCreatesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try cardFolders(Ident.lane1, in: fixture) // "Creating-then-abandoning never leaves an empty card behind" — untitled cards exist only // when made deliberately (04-interactions.md ▸ Grammar). store.transient.beginPlaceholder(inLane: lane1) store.transient.updateDraft(" ") #expect(store.commitPlaceholder() == nil) #expect(try cardFolders(Ident.lane1, in: fixture) == before) #expect(store.transient.newCardPlaceholder == nil) #expect(store.banners.oneShots.isEmpty) } @Test("A commit into a lane the snapshot has lost discards the draft") func vanishedLaneDiscards() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginPlaceholder(inLane: ItemID(rawValue: Ident.indexless)) store.transient.updateDraft("Fix login") #expect(store.commitPlaceholder() == nil) #expect(!fixture.exists(Ident.indexless)) #expect(store.transient.newCardPlaceholder == nil) #expect(store.banners.oneShots.isEmpty) } @Test("A failed create discards the placeholder and banners the failure") func aFailedCreateDiscards() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) // Read-execute only: the lane loads and renders, but no folder can be minted inside it. let laneFolder = fixture.url(Ident.lane2) try FileManager.default.setAttributes([.posixPermissions: 0o555], ofItemAtPath: laneFolder.path) defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: laneFolder.path) } store.transient.beginPlaceholder(inLane: lane2) store.transient.updateDraft("Fix login") #expect(store.commitPlaceholder() == nil) // Settled in 02-architecture.md § Layering: "if the Writer create throws after the title // commits, the create flow discards the placeholder … the overlay never waits for a card // that cannot arrive". The failure is the banner's, not the overlay's. #expect(store.transient.newCardPlaceholder == nil) #expect(store.banners.oneShots.count == 1) #expect(store.banners.oneShots.first?.error.operation == .createCard) } @Test("A read-only board refuses the create without a second banner") func readOnlyBoardRefusesQuietly() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.enterVanishedRootLock() let before = try cardFolders(Ident.lane1, in: fixture) store.transient.beginPlaceholder(inLane: lane1) store.transient.updateDraft("Fix login") #expect(store.commitPlaceholder() == nil) #expect(try cardFolders(Ident.lane1, in: fixture) == before) #expect(store.transient.newCardPlaceholder == nil) #expect(store.banners.oneShots.isEmpty) } @Test("A committed placeholder is not committed twice") func commitIsIdempotent() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) store.transient.beginPlaceholder(inLane: lane1) store.transient.updateDraft("Fix login") let created = try #require(store.commitPlaceholder()) let after = try cardFolders(Ident.lane1, in: fixture) // Return commits and the field's focus-loss handler fires an instant later; the second call // must not file a duplicate. #expect(store.commitPlaceholder() == nil) #expect(try cardFolders(Ident.lane1, in: fixture) == after) #expect(store.transient.newCardPlaceholder?.phase == .awaitingArrival(created)) } } // MARK: - Lane reorder @MainActor @Suite("BoardStore ▸ lane reorder") struct LaneReorderWriteTests { @Test("A lane moved to the head takes a rank before every sibling") func movesToTheHead() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) // lane1 @1024, lane2 @2048, lane3 @1024 (`Item.uneditable`'s own order; the folder-name // tie-break puts it after lane1). Moving the last lane to index 0 puts it at min − 1024. store.moveLane(lane2, toIndex: 0) let model = try load(fixture) #expect(model.lanes.map(\.id) == [lane2, lane1, lane3]) #expect(model.lanes.first { $0.id == lane2 }?.order == 0) } @Test("A lane moved between two siblings takes their midpoint") func movesBetweenSiblings() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "One")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Two")) try fixture.item(Ident.lane3, Item.rich(order: "3072", title: "Three")) let store = try BoardStore(rootURL: fixture.root) // With lane1 removed the remaining pair is [lane2 @2048, lane3 @3072]; landing at index 1 // is between them. store.moveLane(lane1, toIndex: 1) let model = try load(fixture) #expect(model.lanes.map(\.id) == [lane2, lane1, lane3]) #expect(model.lanes.first { $0.id == lane1 }?.order == 2560) // "A reorder rewrites only the moved item's `index.md`" — the neighbours keep their ranks. #expect(model.lanes.first { $0.id == lane2 }?.order == 2048) #expect(model.lanes.first { $0.id == lane3 }?.order == 3072) } @Test("A lane moved to the end is appended past every sibling") func movesToTheEnd() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "One")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Two")) let store = try BoardStore(rootURL: fixture.root) store.moveLane(lane1, toIndex: 1) let model = try load(fixture) #expect(model.lanes.map(\.id) == [lane2, lane1]) #expect(model.lanes.first { $0.id == lane1 }?.order == 3072) } @Test("A drag that ends where it started writes nothing at all") func unchangedIndexIsANoOp() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(Ident.lane1) // Index 0 is where lane1 already sits, counted with itself removed — the release must not // stamp `modified` or mint a commit. store.moveLane(lane1, toIndex: 0) #expect(try fixture.indexData(Ident.lane1) == before) // The writer's temp files are hidden, so only a listing that sees them can prove there is // no residue from a write that should never have started. #expect(try !fixture.entryNames(Ident.lane1).contains { $0.hasPrefix(".") }) #expect(store.banners.oneShots.isEmpty) } @Test("Exhausted precision compacts the board and then places the lane") func exhaustedPrecisionRenumbers() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) // Two lanes on adjacent Doubles: no rank exists between them (01-storage-format.md § // Ordering's renumber trigger), which is exactly what the compaction fallback is for. try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "One")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Two")) try fixture.item(Ident.lane3, Item.rich(order: "2048.0000000000005", title: "Three")) let store = try BoardStore(rootURL: fixture.root) #expect(Ranks.midpoint(between: 2048, and: 2048.0000000000005) == nil, "the gap really is exhausted") // lane1 to the slot between lane2 and lane3. store.moveLane(lane1, toIndex: 1) let model = try load(fixture) #expect(model.lanes.map(\.id) == [lane2, lane1, lane3]) // The compaction runs first and is **sequence-preserving** — it rewrites the ladder to // 1024/2048/3072 in the display order the board already had (lane1, lane2, lane3), so // nothing visibly moves. Only then is the dragged lane placed, at the midpoint of the fresh // gap between lane2 and lane3. #expect(model.lanes.first { $0.id == lane2 }?.order == 2048) #expect(model.lanes.first { $0.id == lane1 }?.order == 2560) #expect(model.lanes.first { $0.id == lane3 }?.order == 3072, "the exhausted rank was compacted away rather than worked around") } @Test("A lane that is not on the board is a no-op, and a read-only board refuses quietly") func unknownLaneAndLockedBoard() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(Ident.lane1) store.moveLane(ItemID(rawValue: Ident.indexless), toIndex: 0) #expect(try fixture.indexData(Ident.lane1) == before) store.enterVanishedRootLock() store.moveLane(lane1, toIndex: 2) #expect(try fixture.indexData(Ident.lane1) == before) #expect(store.banners.oneShots.isEmpty) #expect(store.bannerRows.contains { $0.id == "read-only-lock" }) } } // MARK: - New lane @MainActor @Suite("BoardStore ▸ new lane") struct NewLaneWriteTests { @Test("New Lane appends an untitled lane at the board's right end") func createsAnUntitledLane() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "One")) let store = try BoardStore(rootURL: fixture.root) store.createLane() let model = try load(fixture) #expect(model.lanes.count == 2) let made = try #require(model.lanes.last) #expect(made.id != lane1) // No `title` key at all — the folder-name fallback is a board-level rule, and a lane with no // title renders the untitled placeholder until Board ▸ Rename gives it one. #expect(made.title.isMissing) #expect(made.order == 2048) #expect(store.banners.oneShots.isEmpty) } @Test("New Lane is the way out of a zero-lane board") func createsTheFirstLane() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let store = try BoardStore(rootURL: fixture.root) #expect(store.snapshot.lanes.isEmpty) store.createLane() let model = try load(fixture) #expect(model.lanes.count == 1) #expect(model.lanes.first?.order == 1024, "an empty parent's first child lands at the board convention") } @Test("A read-only board refuses the create without a second banner") func readOnlyBoardRefusesQuietly() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let store = try BoardStore(rootURL: fixture.root) store.enterVanishedRootLock() store.createLane() #expect(try load(fixture).lanes.isEmpty) #expect(store.banners.oneShots.isEmpty) } }