Files
lanework/KanbanTests/TaskCheckboxWriteTests.swift
T
rzen 6dc84176fb Build Preview mode rendering
The card body's resting state: swift-markdown (pinned 0.8.0, smart
typography off — Preview renders the bytes on disk) parsed into a pure
BodyMarkup model with UTF-8 source offsets, rendered on one hosted
TextKit 1 NSTextView — chosen because find-in-text is NSTextFinder,
checkbox clicks reuse AppKit character hit-testing, links are .link
attributes, and NSTextTable's automatic layout is exactly the
columns-sized-to-contents rule. The GFM subset renders per 05; HTML
stays verbatim code-styled text; relative images resolve against the
card folder while remote URLs are never fetched, drawing a quiet chip
instead. Task checkboxes are live: a click flips exactly one byte
through a fresh-read, refuse-uneditable, stamp, atomic-replace write —
the app's only offset-addressed write, so a moved target refuses as
staleTarget and what the user saw decides the direction, netting one
toggle on a double-click. Empty bodies open in Edit per CardBodyMode's
opening rule, applied once; the Edit surface itself stays an honest
read-only stub until its card. FindCommand prefers the card body's
find over board search when a card window is focused.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 09:59:18 -04:00

347 lines
15 KiB
Swift

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)
}
}