import Foundation import Testing @testable import Kanban /// The card window's raw-source outlet (05-card-window.md ▸ Raw source outlet) — the escape hatch /// that "keeps *everything* — unknown keys, exotic formatting — reachable in-app". /// /// Its promise is a byte one, so this suite reads bytes: what Apply writes must be the buffer's own /// bytes and nothing else, which means **no `modified` stamp and no cleared `modified-by`** — the one /// write in the app of which both are true (01-storage-format.md § Frontmatter settles each /// explicitly). The negative half matters as much: a proposal the loader would refuse must leave the /// file untouched, and "the loader" has to mean the same code the loader runs, not a second parser /// that agrees with it today. /// /// `WriterFixture`, `Ident`, `Item` and `writeFailure` come from `WriterTestSupport.swift`. // MARK: - Fixture /// The card the outlet opens, carrying everything a verbatim write must be able to preserve *and* /// everything a stamping write would have destroyed: an unknown key with an inline comment, a second /// unknown key in a shape the app never writes, an old `modified`, and a foreign `modified-by`. private let editableCard = """ --- schema: 1 title: Notes order: 1024 project: lanework # agent overlay labels: [a, b, c] created: 2026-01-01T09:00:00Z modified: 2026-02-02T09:00:00Z modified-by: claude --- # Notes Some *prose*. """ /// A proposal in the shape only this outlet can produce: a hand-added key, a comment above it, /// blank lines inside the frontmatter, an old `modified` left exactly as the user found it, a /// `modified-by` they typed themselves — and **no final newline**, the byte a re-serializing writer /// would have added back. private let handEditedSource = """ --- schema: 1 title: Notes # the user's own comment sphere: work project: lanework # agent overlay labels: [a, b, c] order: 1024 created: 2026-01-01T09:00:00Z modified: 2026-02-02T09:00:00Z modified-by: rzen --- # Notes Rewritten by hand. """ private let cardPath = "\(Ident.lane1)/\(Ident.card1)" @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(cardPath, editableCard) return fixture } private func modificationDate(_ fixture: WriterFixture, _ relativePath: String) throws -> Date? { let url = fixture.url(relativePath).appendingPathComponent("index.md") return try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date } /// Filesystem timestamps are coarse; a write inside the same tick would be invisible to an `mtime` /// assertion either way it goes. private func settleClock() { Thread.sleep(forTimeInterval: 0.05) } // MARK: - Validation, through the loader's own parse @MainActor @Suite("Raw source ▸ validation") struct RawSourceValidationTests { @Test("A card file with unknown keys and exotic formatting is valid — that is the whole point") func theOutletValidatesWhatItExistsToWrite() throws { let document = try BoardLoader.validateCardIndex(Data(handEditedSource.utf8), path: "index.md") #expect(document.title.value == "Notes") // Unknown keys are not merely tolerated, they are the outlet's reason for being: "the write // path for frontmatter the app doesn't own" (05 ▸ Details). #expect(document.value(for: "sphere") == .string("work")) } /// **A card's `schema` and `order` are both optional** (01-storage-format.md § Frontmatter and /// § Ordering, re-ruled 2026-07-31) — the outlet's gate is the loader's rule, so it moved with /// it: a card applied without either lands at its lane's bottom, read as schema 1, and gains a /// real rank on its next touch. @Test("The optional fields are optional, with the loader's own rule") func schemaAndOrderAreOptional() throws { for text in ["---\ntitle: x\norder: 1\n---\nbody\n", "---\nschema: 1\ntitle: x\n---\nbody\n", "---\ntitle: x\n---\nbody\n"] { #expect(throws: Never.self) { try BoardLoader.validateCardIndex(Data(text.utf8), path: "index.md") } } // What still refuses: a value that is there and unreadable, and a card from a newer app — // which this one has no honest way to rewrite. let malformed = validationFailure("---\nschema: one\norder: 1\n---\nbody\n") #expect(malformed?.reason == .malformedSchema(raw: "one")) let future = validationFailure("---\nschema: 99\norder: 1\n---\nbody\n") #expect(future?.reason == .schemaNewerThanApp(found: 99)) } @Test("Malformed YAML refuses with the loader's error, line number and all") func malformedYAMLRefuses() { let error = validationFailure("---\nschema: 1\norder: 1\n bad: [unclosed\n---\nbody\n") guard case let .unparseableYAML(_, line) = error?.reason else { Issue.record("expected unparseable YAML, got \(String(describing: error?.reason))") return } // The taxonomy's display text is what the alert shows — "detailed alert" means this detail. #expect(error?.reason.description.contains("unparseable YAML") == true) #expect(line != nil, "the alert names the line the user has to go and look at") } @Test("A file with no frontmatter at all refuses") func missingDelimitersRefuse() { let error = validationFailure("just a body, no frontmatter\n") #expect(error?.reason.description.contains("---") == true) } @Test("A BOM is refused — the same rejection the loader makes, not a second rule") func aBOMIsRefused() { var bommed = Data([0xEF, 0xBB, 0xBF]) bommed.append(Data("---\nschema: 1\norder: 1\n---\nbody\n".utf8)) let error = validationFailureData(bommed) // "A BOM'd file fails the frontmatter delimiter and is rejected the same way, deliberately" // (01-storage-format.md § Fractal layout ▸ Rules) — so the refusal is the delimiter's, which // is exactly what proves the outlet is not re-implementing the encoding rules. #expect(error?.reason.description.contains("---") == true) } @Test("Bytes that are not UTF-8 are refused before anything tries to parse them") func invalidUTF8IsRefused() { var invalid = Data("---\nschema: 1\norder: 1\ntitle: ".utf8) invalid.append(contentsOf: [0xFF, 0xFE, 0x80]) invalid.append(Data("\n---\nbody\n".utf8)) let error = validationFailureData(invalid) #expect(error?.reason == .unparseableYAML(message: "file is not UTF-8", line: nil)) } @Test("A flow-mapping frontmatter is valid here, though no other write in the app may touch it") func theUneditableShapeIsWritableAsSource() throws { // `BoardWriter.writeBody` refuses this shape — a surgical span edit of it cannot be // expressed. Raw source replaces the whole file, so it is precisely the surface that can get // a user *out* of such a file. Nothing here checks `uneditableShape`, deliberately. let document = try BoardLoader.validateCardIndex( Data("---\n{schema: 1, order: 1024, title: Odd}\n---\nodd body\n".utf8), path: "index.md" ) #expect(document.uneditableShape != nil, "still uneditable in place — and still writable as source") } private func validationFailure(_ text: String) -> BoardLoadError? { validationFailureData(Data(text.utf8)) } private func validationFailureData(_ data: Data) -> BoardLoadError? { do { _ = try BoardLoader.validateCardIndex(data, path: "index.md") Issue.record("expected the validation to refuse, but it passed") return nil } catch { return error } } } // MARK: - The Writer @MainActor @Suite("BoardWriter ▸ raw source") struct RawSourceWriterTests { @Test("Reading gives the file's literal text — comments, CRLF, missing final newline and all") func theReadIsByteHonest() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo")) // No final newline, mixed line endings, a comment: everything a round-trip through a parser // might tidy away. let odd = "---\r\nschema: 1\r\norder: 1024\n# comment\ntitle: Odd\r\n---\r\nbody with no final newline" try fixture.item(cardPath, odd) #expect(try BoardWriter.readRawSource(ofCard: fixture.url(cardPath)) == odd) } @Test("A file that is not UTF-8 refuses to open as source rather than opening a lie") func nonUTF8RefusesToOpen() throws { let fixture = try makeBoard() defer { fixture.tearDown() } var bytes = Data("---\nschema: 1\norder: 1024\ntitle: ".utf8) bytes.append(contentsOf: [0xFF, 0xFE]) bytes.append(Data("\n---\nbody\n".utf8)) try fixture.item(cardPath, bytes: bytes) var thrown: BoardWriteError? do { _ = try BoardWriter.readRawSource(ofCard: fixture.url(cardPath)) Issue.record("expected the read to refuse") } catch { thrown = error } #expect(thrown?.reason == .unreadable(message: "file is not UTF-8")) } @Test("Only a card's file is reachable — not a lane's, not the board's") func onlyCardsAreReachable() throws { let fixture = try makeBoard() defer { fixture.tearDown() } for folder in [fixture.root, fixture.url(Ident.lane1)] { let error = writeFailure { try BoardWriter.writeRawSource(inCard: folder, text: editableCard) } if case .unreadable = error?.reason {} else { Issue.record("expected an unreadable refusal, got \(String(describing: error?.reason))") } } } @Test("Apply writes the buffer's bytes exactly — no stamp, no cleared modified-by, no added newline") func applyIsByteForByte() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let wrote = try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: handEditedSource) #expect(wrote) // The whole contract in one assertion: what is on disk *is* the proposal. #expect(try fixture.indexData(cardPath) == Data(handEditedSource.utf8)) // And the three things a composed app write would have done to it, spelled out because each // is settled prose rather than an implementation detail: let after = try fixture.indexText(cardPath) // 1. `modified` untouched — "Two designed app writes therefore don't bump it, deliberately: // **raw-source Apply** writes the validated buffer byte-for-byte (the verbatim contract // outranks stamping)" (01-storage-format.md § Frontmatter). #expect(after.contains("modified: 2026-02-02T09:00:00Z")) // 2. `modified-by` kept — "One carve-out: **raw-source Apply** … writes byte-for-byte and does // *not* clear a stamp the user typed or kept" (01), and 05 from the other side. #expect(after.contains("modified-by: rzen")) // 3. No final newline invented. #expect(!after.hasSuffix("\n")) } @Test("Byte-for-byte still means the file was rewritten — mtime moves, content is the proposal") func aRealApplyTouchesTheFile() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try modificationDate(fixture, cardPath) settleClock() try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: handEditedSource) // The stamping choice is about *content*: `modified` describes the item, `mtime` describes // the file, and the file genuinely changed. Pinning both directions is what keeps a future // "helpful" stamp from passing the byte assertion by luck. #expect(try modificationDate(fixture, cardPath) != before) #expect(try fixture.indexData(cardPath) == Data(handEditedSource.utf8)) } @Test("Applying the text the file already has writes nothing at all — bytes and mtime") func anUnchangedBufferIsNeverReSerialized() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try fixture.indexData(cardPath) let mtime = try modificationDate(fixture, cardPath) settleClock() let wrote = try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: editableCard) #expect(!wrote, "Apply on a buffer that was only read must not churn the file") #expect(try fixture.indexData(cardPath) == before) #expect(try modificationDate(fixture, cardPath) == mtime) } @Test("A proposal that would not load is refused, and the file keeps every byte it had") func aRefusedProposalWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try fixture.indexData(cardPath) let mtime = try modificationDate(fixture, cardPath) settleClock() let error = writeFailure { try BoardWriter.writeRawSource( inCard: fixture.url(cardPath), text: "---\nschema: 1\n order: [unclosed\n---\nbody\n" ) } guard case let .invalidSource(loadError) = error?.reason else { Issue.record("expected an invalid-source refusal, got \(String(describing: error?.reason))") return } if case .unparseableYAML = loadError.reason {} else { Issue.record("expected the loader's own parse error, got \(loadError.reason)") } #expect(try fixture.indexData(cardPath) == before) #expect(try modificationDate(fixture, cardPath) == mtime) } @Test("A BOM'd proposal is refused by the same rule, and writes nothing") func aBOMdProposalWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try fixture.indexData(cardPath) let error = writeFailure { try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: "\u{FEFF}" + editableCard) } if case .invalidSource = error?.reason {} else { Issue.record("expected an invalid-source refusal, got \(String(describing: error?.reason))") } #expect(try fixture.indexData(cardPath) == before) } @Test("A failure names the card by the title the file still carries") func failuresNameTheCard() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let folder = fixture.url(cardPath) // Unwritable folder: the read, the title enrichment and the validation all succeed, and then // the atomic replace cannot land its temp file. try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: folder.path) let error = writeFailure { try BoardWriter.writeRawSource(inCard: folder, text: handEditedSource) } // The title on the window — read off disk, never off the buffer, which may propose a name // that never landed (`WriteOperation.rawSource`). #expect(error?.operation == .rawSource(title: "Notes")) #expect( BannerCenter.headline(for: try #require(error)) .hasPrefix("Couldn't apply source changes to 'Notes'") ) } @Test("A flow-mapping card — unwritable by every other path — is rewritable as source") func theEscapeHatchEscapes() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let path = "\(Ident.lane1)/\(Ident.card3)" try fixture.item(path, "---\n{schema: 1, order: 3072, title: Odd}\n---\nodd body\n") // `writeBody` refuses this file outright; the outlet is how a user gets out of it. #expect(writeFailure { try BoardWriter.writeBody(inItemFolder: fixture.url(path), body: "new") } != nil) let repaired = "---\nschema: 1\norder: 3072\ntitle: Odd\n---\nodd body\n" try BoardWriter.writeRawSource(inCard: fixture.url(path), text: repaired) #expect(try fixture.indexData(path) == Data(repaired.utf8)) } } // MARK: - Through the store @MainActor @Suite("BoardStore ▸ raw source") struct RawSourceStoreTests { @Test("Reading gives the file, and Apply puts the buffer on disk verbatim") func theStoreReadsAndApplies() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let card = ItemID(rawValue: Ident.card1) #expect(store.readCardSource(inCard: card) == .read(editableCard)) #expect(store.applyCardSource(inCard: card, text: handEditedSource) == .applied) // Read back off disk, never through the snapshot: the one-way flow means the snapshot only // catches up when the watcher's reload lands (02-architecture.md). #expect(try fixture.indexData(cardPath) == Data(handEditedSource.utf8)) } @Test("Applying the file's own text reports unchanged and writes nothing") func anUnchangedApplyWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(cardPath) let mtime = try modificationDate(fixture, cardPath) settleClock() #expect(store.applyCardSource(inCard: ItemID(rawValue: Ident.card1), text: editableCard) == .unchanged) #expect(try fixture.indexData(cardPath) == before) #expect(try modificationDate(fixture, cardPath) == mtime) } @Test("An invalid buffer is refused before any bracket opens — no write, no banner") func anInvalidBufferPostsNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(cardPath) let outcome = store.applyCardSource( inCard: ItemID(rawValue: Ident.card1), text: "---\nschema: unreadable\norder: 1\n---\nbody\n" ) #expect(outcome == .invalid(BoardLoadError(path: "index.md", reason: .malformedSchema(raw: "unreadable")))) #expect(try fixture.indexData(cardPath) == before) // The alert is the surfacing for this one — a banner as well would say the same thing twice, // and a write that never started is not a failed write. #expect(store.banners.oneShots.isEmpty) } @Test("A tombstoned card takes no Apply — a stale buffer never undeletes a card") func aTombstonedCardIsRefused() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.move(cardPath, toTrash: Ident.card1) let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(".trash/\(Ident.card1)") let card = ItemID(rawValue: Ident.card1) // 05 ▸ Deletion & lifecycle: "An open raw-source buffer discards instead: its Apply would // write a whole stale `index.md` over the trashed card — a delete is never fought by a stale // buffer." The Edit buffer's flush is the opposite rule, deliberately, which is why the two // walks differ (`boardItem` vs `cardBodyTarget`). #expect(store.readCardSource(inCard: card) == .vanished) #expect(store.applyCardSource(inCard: card, text: handEditedSource) == .vanished) #expect(try fixture.indexData(".trash/\(Ident.card1)") == before) } @Test("A card that is not in the board at all is vanished too") func anAbsentCardIsVanished() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) #expect(store.readCardSource(inCard: ItemID(rawValue: Ident.card4)) == .vanished) #expect(store.applyCardSource(inCard: ItemID(rawValue: Ident.card4), text: handEditedSource) == .vanished) } @Test("A read-only board suspends the Apply rather than failing it") func theLockSuspends() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(cardPath) let card = ItemID(rawValue: Ident.card1) store.enterVanishedRootLock() // Apply is a mutation, however literal, so the lock refuses it like every other write — and // the buffer's owner reads this as "hold the text", the standing lock row being the message. #expect(store.applyCardSource(inCard: card, text: handEditedSource) == .suspended(.vanishedRoot)) #expect(try fixture.indexData(cardPath) == before) #expect(store.banners.oneShots.isEmpty, "the lock's row is the message; a refused Apply posts nothing") // Reading is not a write: a locked board still opens its source, which is how the text gets // copied out (02-architecture.md § the lock's scope). #expect(store.readCardSource(inCard: card) == .read(editableCard)) } @Test("A failed write reports the error, and the banner has it") func aFailedApplyReports() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) try FileManager.default.setAttributes( [.posixPermissions: 0o500], ofItemAtPath: fixture.url(cardPath).path ) let outcome = store.applyCardSource(inCard: ItemID(rawValue: Ident.card1), text: handEditedSource) guard case let .failed(error) = outcome else { Issue.record("expected a failure, got \(outcome)") return } #expect(error.operation == .rawSource(title: "Notes")) #expect(store.banners.oneShots.contains { BannerCenter.headline(for: $0.error).hasPrefix("Couldn't apply source changes to 'Notes'") }) } } // MARK: - The session /// The outlet's state machine, driven through **the window's own wiring** /// (`CardWindowHost.configureRawSource`) rather than a re-typed copy of it: the ordering it encodes — /// flush, *then* read fresh — is invisible in a running window until it is wrong. @MainActor @Suite("Card raw source ▸ session") struct RawSourceSessionTests { /// Everything a card window holds, wired exactly as the host wires it. private struct Rig { let fixture: WriterFixture let store: BoardStore let presentation: CardBodyPresentation let body: CardBodyEditSession let raw: CardRawSourceSession } private func makeRig() throws -> Rig { let fixture = try makeBoard() let store = try BoardStore(rootURL: fixture.root) let cardID = ItemID(rawValue: Ident.card1) let presentation = CardBodyPresentation() let body = CardBodyEditSession() body.save = { [weak store] text in guard let store else { return .vanished } return store.writeCardBody(inCard: cardID, body: text) } body.adopt(diskBody: try FrontmatterDocument.parse(fixture.indexText(cardPath)).body) presentation.flushEdits = { body.endEditSession() } let raw = CardRawSourceSession() CardWindowHost.configureRawSource(raw, body: body, presentation: presentation, store: store, cardID: cardID) return Rig(fixture: fixture, store: store, presentation: presentation, body: body, raw: raw) } @Test("Entering flushes the Edit buffer first, then reads what the flush put on disk") func enteringFlushesThenReadsFresh() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } rig.presentation.setMode(.edit) // Unsaved keystrokes, well inside the debounce: nothing has reached disk yet. rig.body.edited("# Notes\n\nTyped but not yet saved.\n") #expect(rig.body.isDirty) #expect(!(try rig.fixture.indexText(cardPath).contains("not yet saved"))) #expect(rig.raw.enter()) // The ordering, both halves. If the read had run first the editor would show a file the app // was a moment from overwriting from a buffer the user could no longer see. #expect(try rig.fixture.indexText(cardPath).contains("Typed but not yet saved.")) #expect(rig.raw.text.contains("Typed but not yet saved.")) #expect(rig.raw.text == (try rig.fixture.indexText(cardPath)), "the buffer is the file, byte for byte") #expect(!rig.body.isDirty) } @Test("Entering from Edit leaves the body column in Preview — and leaving raw source lands there") func rawSourceExitsToPreview() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } rig.presentation.setMode(.edit) rig.raw.enter() // 05 lists "raw-source entry" among the three events that *leave Edit*, so entering genuinely // leaves it and Preview — the resting state — is what the exit reveals. Recorded as a choice // because 05 does not name the exit mode itself. #expect(rig.presentation.mode == .preview) rig.raw.cancel() #expect(rig.presentation.mode == .preview) #expect(!rig.raw.isActive) } @Test("An empty body after Apply does not drag the window into Edit") func theOpeningRuleDoesNotRunAgain() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } // The window opened on a card with a body, so it opened in Preview — once, per the rule. #expect(rig.presentation.openIfNeeded(body: "# Notes\n") == .preview) rig.raw.enter() // The user deletes the body in source mode and applies. rig.raw.text = "---\nschema: 1\ntitle: Notes\norder: 1024\n---\n" #expect(rig.raw.applyAndLeave()) // "The rule is about *opening* a card" (`CardBodyPresentation.openIfNeeded`), and a raw exit // is not an open: the user gets the blank Preview they just wrote, and ⌘E. #expect(rig.presentation.mode == .preview) #expect(try FrontmatterDocument.parse(rig.fixture.indexText(cardPath)).body.isEmpty) } @Test("Apply closes the outlet and lands the bytes") func applyCommitsAndLeaves() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } rig.raw.enter() rig.raw.text = handEditedSource #expect(rig.raw.applyAndLeave()) #expect(!rig.raw.isActive) #expect(rig.raw.alert == nil) #expect(try rig.fixture.indexData(cardPath) == Data(handEditedSource.utf8)) } @Test("A failed validation keeps source mode open, with the alert and the text") func aFailedValidationStays() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } let before = try rig.fixture.indexData(cardPath) rig.raw.enter() let broken = "---\nschema: 1\n order: [unclosed\n---\nbody\n" rig.raw.text = broken #expect(!rig.raw.applyAndLeave()) // "A failed validation keeps source mode open (toggle stays checked) with the alert" (05). #expect(rig.raw.isActive, "the toggle stays checked") #expect(rig.raw.text == broken, "and the user's text is still in front of them") guard case .invalid = rig.raw.alert else { Issue.record("expected a validation alert, got \(String(describing: rig.raw.alert))") return } #expect(try rig.fixture.indexData(cardPath) == before) // OK returns to editing the raw text — nothing else moves. rig.raw.dismissAlert() #expect(rig.raw.alert == nil) #expect(rig.raw.isActive) #expect(rig.raw.text == broken) } @Test("Cancel discards without ceremony — nothing written, nothing asked") func cancelDiscards() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } let before = try rig.fixture.indexData(cardPath) rig.raw.enter() rig.raw.text = handEditedSource rig.raw.cancel() #expect(!rig.raw.isActive) #expect(rig.raw.applyAttempts == 0, "Cancel is not a write that happened to fail — it never asked") #expect(try rig.fixture.indexData(cardPath) == before) #expect(rig.raw.text.isEmpty, "and the buffer is gone with it, so a re-entry reads disk afresh") } @Test("A file that cannot be read does not open source mode") func anUnreadableFileDoesNotOpen() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } var bytes = Data("---\nschema: 1\norder: 1024\ntitle: ".utf8) bytes.append(contentsOf: [0xFF, 0xFE]) bytes.append(Data("\n---\nbody\n".utf8)) try rig.fixture.item(cardPath, bytes: bytes) #expect(!rig.raw.enter()) // Settled here, 05 being silent: an editor over lossily-decoded bytes would make the outlet's // one promise false, since Apply writes back whatever is in it. #expect(!rig.raw.isActive, "the toggle stays unchecked") guard case .unreadable = rig.raw.alert else { Issue.record("expected an unreadable alert, got \(String(describing: rig.raw.alert))") return } } @Test("A window that has not joined its board opens nothing and writes nothing") func unwiredSeamsFailClosed() { let raw = CardRawSourceSession() #expect(!raw.enter()) #expect(!raw.isActive) #expect(!raw.applyAndLeave(), "and an Apply with no outlet open is not an Apply") } @Test("Re-entering after an Apply reads disk again rather than reusing the old buffer") func reEntryReadsFresh() throws { let rig = try makeRig() defer { rig.fixture.tearDown() } rig.raw.enter() rig.raw.text = handEditedSource rig.raw.applyAndLeave() // Somebody else — an agent, a pull — rewrites the card while no window holds it. let foreign = handEditedSource.replacingOccurrences(of: "Rewritten by hand.", with: "Rewritten by them.") try rig.fixture.item(cardPath, foreign) rig.raw.enter() #expect(rig.raw.text == foreign, "the outlet never shows an in-memory snapshot of the file") } } // MARK: - The menu rows @MainActor @Suite("Card view commands ▸ validation") struct CardViewCommandValidationTests { @Test("Edit Body needs a card window, and disables while source mode is active") func editBodyDisablesUnderRawSource() { let presentation = CardBodyPresentation() let raw = CardRawSourceSession() raw.read = { .read("---\nschema: 1\norder: 1\n---\nbody\n") } raw.apply = { _ in .applied } // No card window in front: scope alone disables the row. #expect(!EditBodyCommand.isEnabled(body: nil, rawSource: nil)) #expect(!EditBodyCommand.isEnabled(body: nil, rawSource: raw)) // A card window, not in source mode: live. #expect(EditBodyCommand.isEnabled(body: presentation, rawSource: raw)) raw.enter() // "View ▸ Edit Body (⌘E) disables while source mode is active, matching its toolbar item" // (05 ▸ Raw source outlet) — the two would otherwise be editing the same bytes from two // surfaces, one of which is not on screen. #expect(raw.isActive) #expect(!EditBodyCommand.isEnabled(body: presentation, rawSource: raw)) raw.cancel() #expect(EditBodyCommand.isEnabled(body: presentation, rawSource: raw)) } }