import Foundation import Testing @testable import Kanban /// The card window's **one write into a body**: a Preview task-list checkbox being ticked /// (05-card-window.md ▸ Preview — "clicking a `- [ ]` / `- [x]` checkbox flips exactly that marker /// in the source — a single-character textual edit; every other byte of the body is untouched"). /// /// Every other write in the app edits *frontmatter* by line span, and the round-trip guarantee falls /// out of never re-serializing the body at all. This one edits the body, which means the guarantee /// has to be earned rather than inherited — so the assertions here are byte comparisons of the body /// against a literal expectation, not "the checkbox reads as ticked afterwards". /// /// Like the rest of the write suites this drives real files in a temp board and reads back **raw /// bytes**, never a snapshot: the claim is about what is on disk. `WriterFixture`, `Ident` and /// `Item` come from `WriterTestSupport.swift`. // MARK: - Fixture /// A body with one of each checkbox state, a nested one, and — deliberately — a literal `[x]` in /// prose that no flip may ever touch. private let checklistBody = """ # Tasks - [ ] first - [x] second - [ ] nested Trailing prose with a [x] literal. """ /// The card, with everything a write must leave alone around the body: an unknown key carrying an /// inline comment, a `created` from before today, and a foreign `modified-by`. private let checklistCard = """ --- schema: 1 title: Checklist order: 1024 project: lanework # agent overlay created: 2026-01-01T09:00:00Z modified: 2026-02-02T09:00:00Z modified-by: claude --- \(checklistBody) """ private let cardPath = "\(Ident.lane1)/\(Ident.card1)" private let siblingPath = "\(Ident.lane1)/\(Ident.card2)" @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, checklistCard) try fixture.item(siblingPath, Item.rich(order: "2048", title: "Untouched")) return fixture } /// The card's body as it is on disk right now — split off at the closing delimiter by the same /// parser the writer used, so "the body" means the same thing in the test as in the app. private func body(of fixture: WriterFixture, _ relativePath: String) throws -> String { try FrontmatterDocument.parse(fixture.indexText(relativePath)).body } /// The file's frontmatter lines, minus the two the stamp owns — what has to be identical, comment /// and key order included. private func frontmatterLines(_ text: String) -> [String] { let lines = text.components(separatedBy: "\n") guard let closing = lines.dropFirst().firstIndex(where: { $0.trimmingCharacters(in: .whitespaces) == "---" }) else { return lines } return lines[0 ..< closing].filter { !$0.hasPrefix("modified:") && !$0.hasPrefix("modified-by:") } } /// The marker offsets the *parse* produces for a body — the app's own path from a rendered checkbox /// to the byte a click will flip, used here rather than hand-counted numbers so the two halves of /// the feature are tested joined up. private func markerOffsets(in body: String) -> [Int] { func walk(_ blocks: [BodyBlock]) -> [Int] { blocks.flatMap { block -> [Int] in switch block { case let .list(list, _): list.items.flatMap { ($0.task?.markerOffset.map { [$0] } ?? []) + walk($0.blocks) } case let .quote(children, _): walk(children) default: [] } } } return walk(BodyMarkup.parse(body).blocks) } private func modifiedDate(_ fixture: WriterFixture, _ relativePath: String) throws -> Date? { try FrontmatterDocument.parse(fixture.indexText(relativePath)).modified.value } // MARK: - The flip @MainActor @Suite("BoardWriter ▸ toggleTaskMarker") struct ToggleTaskMarkerTests { @Test("Ticking a box changes exactly that byte of the body, and nothing else in the file") func aFlipIsOneByte() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try fixture.indexText(cardPath) let offsets = markerOffsets(in: checklistBody) #expect(offsets.count == 3, "the fixture's three checkboxes should all be located") try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[0], checked: false) // The body, byte for byte, against a literal: the heading, the blank lines, the nested item, // the trailing prose's literal `[x]` and the trailing newline are all still exactly there. #expect(try body(of: fixture, cardPath) == """ # Tasks - [x] first - [x] second - [ ] nested Trailing prose with a [x] literal. """) // And the frontmatter is untouched but for the two keys every app write owns: key order, // the unknown `project` key, its inline comment and `created` all survive. let after = try fixture.indexText(cardPath) #expect(frontmatterLines(after) == frontmatterLines(before)) #expect(after.contains("project: lanework # agent overlay")) #expect(after.contains("created: 2026-01-01T09:00:00Z")) } @Test("The write stamps modified and clears a foreign modified-by") func theWriteStamps() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let offsets = markerOffsets(in: checklistBody) try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[1], checked: true) // A toggle is "an ordinary user edit — the standard atomic write" (05), so it carries the // same stamps every other app write does (01-storage-format.md § Frontmatter). let stamped = try #require(try modifiedDate(fixture, cardPath)) #expect(stamped.timeIntervalSinceNow > -30) #expect(!(try fixture.indexText(cardPath).contains("modified-by"))) } @Test("Flipping twice restores the file's body byte for byte") func aFlipRoundTrips() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let original = try body(of: fixture, cardPath) let offsets = markerOffsets(in: checklistBody) try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[0], checked: false) #expect(try body(of: fixture, cardPath) != original) // The second flip reads the file fresh and finds the marker where the first one left it — // the offset is stable because the edit changed a byte's *value*, never the body's length. try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[0], checked: true) #expect(try body(of: fixture, cardPath) == original) #expect(try Data(body(of: fixture, cardPath).utf8) == Data(original.utf8)) } @Test("A nested checkbox flips, and its parent does not") func nestedMarkersAreTheirOwn() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let offsets = markerOffsets(in: checklistBody) try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[2], checked: false) #expect(try body(of: fixture, cardPath) == """ # Tasks - [ ] first - [x] second - [x] nested Trailing prose with a [x] literal. """) } @Test("No other file is opened, let alone rewritten") func siblingsAreUntouched() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let siblingURL = fixture.url(siblingPath).appendingPathComponent("index.md") let siblingBefore = try Data(contentsOf: siblingURL) let siblingModified = try FileManager.default.attributesOfItem(atPath: siblingURL.path)[.modificationDate] as? Date let laneBefore = try fixture.indexData(Ident.lane1) let offsets = markerOffsets(in: checklistBody) try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[0], checked: false) #expect(try Data(contentsOf: siblingURL) == siblingBefore) // The filesystem's own "did anything happen here" signal, not just the bytes. let siblingAfter = try FileManager.default.attributesOfItem(atPath: siblingURL.path)[.modificationDate] as? Date #expect(siblingAfter == siblingModified) #expect(try fixture.indexData(Ident.lane1) == laneBefore) } @Test("A successful flip leaves no temp file behind") func noResidue() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let offsets = markerOffsets(in: checklistBody) try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[0], checked: false) // Hidden entries included — the writer's temps are dot-prefixed, so only a listing that // sees them can prove there is none. #expect(try fixture.entryNames(cardPath) == ["index.md"]) } } // MARK: - Refusals @MainActor @Suite("BoardWriter ▸ toggleTaskMarker refusals") struct ToggleTaskMarkerRefusalTests { @Test("A state the file no longer agrees with refuses, and writes nothing") func aStaleStateRefuses() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try fixture.indexData(cardPath) let offsets = markerOffsets(in: checklistBody) // The user saw an unticked box; disk says it is ticked. Flipping would undo somebody else's // edit instead of performing this one, so it refuses (`toggleTaskMarker`'s re-verification). let error = writeFailure { try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offsets[1], checked: false) } #expect(error?.operation == .toggleTask(title: "Checklist")) if case .staleTarget = error?.reason {} else { Issue.record("expected a staleTarget refusal, got \(String(describing: error?.reason))") } #expect(try fixture.indexData(cardPath) == before, "a refused write is a write that did not happen") } @Test("An offset that is not a checkbox refuses") func aStaleOffsetRefuses() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let before = try fixture.indexData(cardPath) for offset in [0, 7, 999_999] { let error = writeFailure { try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(cardPath), bodyOffset: offset, checked: false) } if case .staleTarget = error?.reason {} else { Issue.record("expected a staleTarget refusal at \(offset), got \(String(describing: error?.reason))") } } #expect(try fixture.indexData(cardPath) == before) } @Test("Frontmatter that cannot be edited in place refuses before the body is touched") func uneditableFrontmatterRefuses() throws { let fixture = try makeBoard() defer { fixture.tearDown() } // A whole-frontmatter flow mapping: readable, renderable, and unwritable — the settled // readable-but-uneditable rule, which a body edit is no exemption from, because the write // still has to stamp `modified` through the span editor. let path = "\(Ident.lane1)/\(Ident.card3)" try fixture.item(path, "---\n{schema: 1, order: 3072}\n---\n- [ ] task\n") let before = try fixture.indexData(path) let error = writeFailure { try BoardWriter.toggleTaskMarker(inItemFolder: fixture.url(path), bodyOffset: 3, checked: false) } #expect(error?.reason == .uneditableFrontmatter(.keyWithoutOwnLine)) #expect(try fixture.indexData(path) == before) } @Test("A folder that is not a lane or a card refuses") func strayFoldersRefuse() throws { let fixture = try makeBoard() defer { fixture.tearDown() } // The board root is the case that matters: a board's body is its description, no surface // previews it, and the shape guard is what makes this call structurally unable to reach it. let error = writeFailure { try BoardWriter.toggleTaskMarker(inItemFolder: fixture.root, bodyOffset: 3, checked: false) } if case .unreadable = error?.reason {} else { Issue.record("expected an unreadable refusal, got \(String(describing: error?.reason))") } } } // MARK: - Through the store @MainActor @Suite("BoardStore ▸ toggleTaskMarker") struct StoreToggleTaskMarkerTests { @Test("A click through the store lands on disk") func theStoreWritesThrough() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let offsets = markerOffsets(in: checklistBody) store.toggleTaskMarker(inCard: ItemID(rawValue: Ident.card1), bodyOffset: offsets[0], checked: false) // Read back through the loader, never through the store's snapshot: the one-way flow means // the snapshot only catches up when the watcher's reload lands (02-architecture.md). #expect(try body(of: fixture, cardPath).contains("- [x] first")) } @Test("A card that is not in the snapshot is not written to") func aVanishedCardWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let store = try BoardStore(rootURL: fixture.root) let before = try fixture.indexData(cardPath) // An id the board does not hold — the vanished-target guard every gesture in the store // makes, here standing in for a card window whose card left under the click. store.toggleTaskMarker(inCard: ItemID(rawValue: Ident.card4), bodyOffset: 3, checked: false) #expect(try fixture.indexData(cardPath) == before) } @Test("A tombstoned card's checkbox does not write") func aTombstonedCardWritesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try BoardWriter.deleteItem(at: fixture.url(cardPath)) let store = try BoardStore(rootURL: fixture.root) let before = try body(of: fixture, cardPath) let offsets = markerOffsets(in: checklistBody) // Effective liveness, ancestor-walked (`BoardStore.liveItem`): a card in the trash renders // nowhere, so nothing may write through a preview of it. store.toggleTaskMarker(inCard: ItemID(rawValue: Ident.card1), bodyOffset: offsets[0], checked: false) #expect(try body(of: fixture, cardPath) == before) } }