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
This commit is contained in:
2026-07-28 09:59:18 -04:00
parent 7f1adf47c5
commit 6dc84176fb
15 changed files with 2608 additions and 18 deletions
+517
View File
@@ -0,0 +1,517 @@
import Foundation
import Testing
@testable import Kanban
/// The executable spec for the card body's parse render **model** (05-card-window.md Preview).
///
/// The renderer beneath it draws pixels a unit test cannot see; what it draws *from* is this model,
/// and every rule 05 settles about Preview is a fact about the model rather than about the drawing:
/// which constructs are in the subset, that HTML is literal text rather than markup, that an image
/// with a scheme is never fetched, that a link knows whether it is the browser's or the Finder's,
/// and the one that reaches disk where a task checkbox's single byte lives in the source.
///
/// So this file is where those hold still. A regression here is a rendering that would be *wrong*;
/// a regression in the renderer is one that would be ugly.
// MARK: - Reading the model
/// The first block of a parsed body most cases here are one construct, and naming the unwrap once
/// keeps each test to its claim.
private func firstBlock(_ source: String) -> BodyBlock? {
BodyMarkup.parse(source).blocks.first
}
/// Every `BodyInline` flattened to the plain text it carries, containers descended into what the
/// reader ends up seeing, with the structure taken away.
private func plainText(_ inlines: [BodyInline]) -> String {
inlines.map { inline in
switch inline {
case let .text(text): text
case let .code(code): code
case let .html(raw): raw
case let .emphasis(children), let .strong(children), let .strikethrough(children):
plainText(children)
case let .link(_, children): plainText(children)
case let .image(image): image.alt
case .lineBreak: "\n"
case .softBreak: " "
}
}.joined()
}
/// The body slice a block's source range names the assertion that a range is *the* range rather
/// than merely plausible.
private func slice(_ body: String, _ span: BodySpan?) -> String? {
guard let span else { return nil }
let bytes = Array(body.utf8)
guard span.start <= span.end, span.end <= bytes.count else { return nil }
return String(decoding: bytes[span.start ..< span.end], as: UTF8.self)
}
private func span(of block: BodyBlock?) -> BodySpan? {
switch block {
case let .heading(_, _, range), let .paragraph(_, range), let .code(_, _, range),
let .html(_, range), let .thematicBreak(range), let .quote(_, range),
let .list(_, range), let .table(_, range):
range
case nil:
nil
}
}
// MARK: - The subset, construct by construct
@Suite("Body markup — the GFM subset")
struct BodyMarkupSubsetTests {
@Test("Headings carry their level and their inlines")
func headings() {
guard case let .heading(level, inlines, _)? = firstBlock("## Fix *login*\n") else {
Issue.record("expected a heading")
return
}
#expect(level == 2)
#expect(plainText(inlines) == "Fix login")
// The emphasis is structure, not a string: a renderer that lost it would still pass a
// plain-text check.
#expect(inlines.contains { if case .emphasis = $0 { true } else { false } })
}
@Test("Bold, italic, inline code and strikethrough are each their own inline")
func inlineRuns() {
guard case let .paragraph(inlines, _)? = firstBlock("**a** _b_ `c` ~~d~~\n") else {
Issue.record("expected a paragraph")
return
}
#expect(inlines.contains { if case .strong = $0 { true } else { false } })
#expect(inlines.contains { if case .emphasis = $0 { true } else { false } })
#expect(inlines.contains { if case .code("c") = $0 { true } else { false } })
#expect(inlines.contains { if case .strikethrough = $0 { true } else { false } })
}
@Test("A fenced block keeps its language; an indented one is code all the same")
func codeBlocks() {
guard case let .code(fenced, language, _)? = firstBlock("```swift\nlet x = 1\n```\n") else {
Issue.record("expected a fenced code block")
return
}
#expect(language == "swift")
#expect(fenced.hasPrefix("let x = 1"))
// The indented form is the same construct with nothing to label it 05 lists "fenced +
// indented code" as one line for a reason.
guard case let .code(indented, indentedLanguage, _)? = firstBlock(" let y = 2\n") else {
Issue.record("expected an indented code block")
return
}
#expect(indentedLanguage == nil)
#expect(indented.hasPrefix("let y = 2"))
}
@Test("Quotes nest")
func nestedQuotes() {
guard case let .quote(outer, _)? = firstBlock("> outer\n>\n> > inner\n") else {
Issue.record("expected a block quote")
return
}
let inner = outer.compactMap { block -> [BodyBlock]? in
if case let .quote(blocks, _) = block { return blocks }
return nil
}
#expect(inner.count == 1, "a quote inside a quote is a quote inside a quote, not a flattened one")
if case let .paragraph(inlines, _)? = inner.first?.first {
#expect(plainText(inlines) == "inner")
} else {
Issue.record("expected the nested quote's paragraph")
}
}
@Test("An ordered list remembers where it starts; a bullet list does not pretend to")
func lists() {
guard case let .list(ordered, _)? = firstBlock("3. three\n4. four\n") else {
Issue.record("expected an ordered list")
return
}
#expect(ordered.isOrdered)
#expect(ordered.start == 3)
#expect(ordered.items.count == 2)
guard case let .list(bullets, _)? = firstBlock("- one\n- two\n") else {
Issue.record("expected a bullet list")
return
}
#expect(!bullets.isOrdered)
#expect(bullets.items.allSatisfy { $0.task == nil })
}
@Test("A thematic break is its own block")
func thematicBreak() {
guard case .thematicBreak? = firstBlock("---\n") else {
Issue.record("expected a thematic break")
return
}
}
@Test("A GFM table carries per-column alignment, its header, and its rows")
func tables() {
let source = """
| Left | Middle | Right |
| :--- | :----: | ----: |
| a | b | c |
| d | e | f |
"""
guard case let .table(table, _)? = firstBlock(source) else {
Issue.record("expected a table")
return
}
#expect(table.columnCount == 3)
#expect(table.alignments == [.leading, .center, .trailing])
#expect(table.header.map { plainText($0.inlines) } == ["Left", "Middle", "Right"])
#expect(table.rows.count == 2)
#expect(table.rows.first?.map { plainText($0.inlines) } == ["a", "b", "c"])
}
@Test("A table with no alignment row markers leaves its columns unspecified")
func tableWithoutAlignments() {
let source = """
| One | Two |
| --- | --- |
| a | b |
"""
guard case let .table(table, _)? = firstBlock(source) else {
Issue.record("expected a table")
return
}
#expect(table.alignments == [.unspecified, .unspecified])
// One entry per column, always a renderer indexes this array by column and must not have
// to guard every access.
#expect(table.alignments.count == table.columnCount)
}
}
// MARK: - HTML is text
@Suite("Body markup — HTML is never interpreted")
struct BodyMarkupHTMLTests {
@Test("An HTML block is carried verbatim, as literal text")
func htmlBlockIsVerbatim() {
guard case let .html(raw, _)? = firstBlock("<div class=\"x\">\n <b>bold</b>\n</div>\n") else {
Issue.record("expected an HTML block")
return
}
// Verbatim: the angle brackets, the attribute quoting and the inner tag are all still
// characters. Nothing here is a `<b>` the app is about to honour (05 Preview; 00-vision's
// no-web-tech stance).
#expect(raw.contains("<div class=\"x\">"))
#expect(raw.contains("<b>bold</b>"))
}
@Test("Inline HTML is an inline of its own, never a rendered tag")
func inlineHTMLIsVerbatim() {
guard case let .paragraph(inlines, _)? = firstBlock("before <b>x</b> after\n") else {
Issue.record("expected a paragraph")
return
}
let html = inlines.compactMap { inline -> String? in
if case let .html(raw) = inline { return raw }
return nil
}
#expect(html == ["<b>", "</b>"])
// And crucially *not* a strong run: the tags are text and "x" is text beside them.
#expect(!inlines.contains { if case .strong = $0 { true } else { false } })
#expect(plainText(inlines) == "before <b>x</b> after")
}
}
// MARK: - Where a link or an image points
@Suite("Body markup — link and image classification")
struct BodyTargetTests {
@Test("A path with no scheme is relative; anything with one is absolute")
func classification() {
#expect(BodyTarget.classify("attachments/shot.png") == .relative("attachments/shot.png"))
#expect(BodyTarget.classify("./notes.md") == .relative("./notes.md"))
#expect(BodyTarget.classify("../sibling/index.md") == .relative("../sibling/index.md"))
#expect(BodyTarget.classify("https://example.com/x.png") == .absolute("https://example.com/x.png"))
#expect(BodyTarget.classify("mailto:[email protected]") == .absolute("mailto:[email protected]"))
#expect(BodyTarget.classify("file:///tmp/x.png") == .absolute("file:///tmp/x.png"))
// Nothing to point at is a relative nothing, not an absolute one.
#expect(BodyTarget.classify(nil) == .relative(""))
#expect(BodyTarget.classify("") == .relative(""))
}
@Test("A relative image resolves against the card's own folder; a remote one is never fetched")
func imageClassification() {
guard case let .paragraph(local, _)? = firstBlock("![a shot](attachments/shot.png)\n"),
case let .image(localImage)? = local.first
else {
Issue.record("expected a local image")
return
}
#expect(localImage.target == .relative("attachments/shot.png"))
#expect(localImage.alt == "a shot")
guard case let .paragraph(remote, _)? = firstBlock("![alt text](https://example.com/x.png)\n"),
case let .image(remoteImage)? = remote.first
else {
Issue.record("expected a remote image")
return
}
// `.absolute` is the whole of "Preview does no networking": nothing downstream is given a
// loader for this case at all it renders as a chip carrying the alt text (05 Preview).
#expect(remoteImage.target == .absolute("https://example.com/x.png"))
#expect(remoteImage.alt == "alt text")
}
@Test("A link keeps both its destination's kind and its own text")
func linkClassification() {
guard case let .paragraph(inlines, _)? = firstBlock("see [the notes](notes.md) and [home](https://example.com)\n")
else {
Issue.record("expected a paragraph")
return
}
let links = inlines.compactMap { inline -> (BodyTarget, String)? in
if case let .link(target, children) = inline { return (target, plainText(children)) }
return nil
}
#expect(links.count == 2)
#expect(links.first?.0 == .relative("notes.md"))
#expect(links.first?.1 == "the notes")
#expect(links.last?.0 == .absolute("https://example.com"))
}
@Test("Resolution is against the card folder, and a rooted path is taken as written")
func resolution() {
let folder = URL(fileURLWithPath: "/tmp/board/lane/card", isDirectory: true)
#expect(
BodyTarget.relative("attachments/shot.png").resolve(inCardFolder: folder)?.path
== "/tmp/board/lane/card/attachments/shot.png"
)
// Percent-encoding is the author's escaping of a filename, not part of the filename.
#expect(
BodyTarget.relative("attachments/my%20shot.png").resolve(inCardFolder: folder)?.path
== "/tmp/board/lane/card/attachments/my shot.png"
)
// A rooted path names itself; gluing it onto the card folder would name a file nobody meant.
#expect(BodyTarget.relative("/etc/hosts").resolve(inCardFolder: folder)?.path == "/etc/hosts")
#expect(BodyTarget.absolute("https://example.com/x").resolve(inCardFolder: folder)?.scheme == "https")
// Nothing to resolve, and nothing to resolve against.
#expect(BodyTarget.relative("").resolve(inCardFolder: folder) == nil)
#expect(BodyTarget.relative("notes.md").resolve(inCardFolder: nil) == nil)
}
}
// MARK: - Task checkboxes
@Suite("Body markup — task checkboxes")
struct BodyTaskTests {
/// Two items, one of each state, with a nested one under the second.
private static let source = """
- [ ] first
- [x] second
- [ ] nested
"""
private func tasks(of blocks: [BodyBlock]) -> [BodyTask] {
blocks.flatMap { block -> [BodyTask] in
switch block {
case let .list(list, _):
list.items.flatMap { item in (item.task.map { [$0] } ?? []) + tasks(of: item.blocks) }
case let .quote(children, _):
tasks(of: children)
default:
[]
}
}
}
@Test("A task item's checkbox state and its source byte both survive the parse")
func markersAreLocated() {
let markup = BodyMarkup.parse(Self.source)
let found = tasks(of: markup.blocks)
#expect(found.map(\.isChecked) == [false, true, false])
// The offsets, counted by hand from the source above the one number in this model that a
// write will aim at, so it is asserted as a number rather than as "somewhere in the line":
// "- [ ] first\n" `[` at 2, marker at 3
// "- [x] second\n" line starts at 12, marker at 15
// " - [ ] nested\n" line starts at 25, marker at 30
#expect(found.map(\.markerOffset) == [3, 15, 30])
// And each one really is the character between the brackets.
let bytes = Array(Self.source.utf8)
for task in found {
guard let offset = task.markerOffset else {
Issue.record("a parsed checkbox with no located marker")
continue
}
#expect(bytes[offset - 1] == UInt8(ascii: "["))
#expect(bytes[offset + 1] == UInt8(ascii: "]"))
#expect(BodyMarkup.markerState(bytes[offset]) == task.isChecked)
}
}
@Test("An ordinary list item has no checkbox, and a literal [x] in prose is not one")
func nonTasksAreNotTasks() {
#expect(tasks(of: BodyMarkup.parse("- plain\n- also plain\n").blocks).isEmpty)
// The scan is line-scoped and starts at a *task item*: prose that happens to contain the
// three characters is prose (`taskMarkerOffset`).
#expect(tasks(of: BodyMarkup.parse("A sentence with a [x] in it.\n").blocks).isEmpty)
}
@Test("A mixed list keeps its plain items plain")
func mixedList() {
guard case let .list(list, _)? = firstBlock("- [ ] a task\n- not a task\n") else {
Issue.record("expected a list")
return
}
#expect(list.items.count == 2)
#expect(list.items.first?.task?.isChecked == false)
#expect(list.items.last?.task == nil)
}
}
// MARK: - The flip
@Suite("Body markup — flipping a task marker")
struct BodyMarkerFlipTests {
private static let body = "- [ ] first\n- [x] second\n"
@Test("A flip changes exactly one byte, in each direction")
func flipsOneByte() {
guard let ticked = BodyMarkup.flippingTaskMarker(in: Self.body, at: 3, expecting: false) else {
Issue.record("expected the flip to land")
return
}
#expect(ticked == "- [x] first\n- [x] second\n")
guard let unticked = BodyMarkup.flippingTaskMarker(in: Self.body, at: 15, expecting: true) else {
Issue.record("expected the flip to land")
return
}
#expect(unticked == "- [ ] first\n- [ ] second\n")
// The one-byte claim, stated as a byte count rather than inferred from the strings.
let before = Array(Self.body.utf8)
let after = Array(ticked.utf8)
#expect(before.count == after.count)
#expect(zip(before, after).filter { $0 != $1 }.count == 1)
}
@Test("A flip round-trips")
func roundTrips() {
let ticked = BodyMarkup.flippingTaskMarker(in: Self.body, at: 3, expecting: false)
let back = ticked.flatMap { BodyMarkup.flippingTaskMarker(in: $0, at: 3, expecting: true) }
#expect(back == Self.body)
}
@Test("An uppercase X reads as checked and unticks to a space")
func uppercaseMarker() {
#expect(BodyMarkup.flippingTaskMarker(in: "- [X] one\n", at: 3, expecting: true) == "- [ ] one\n")
}
@Test("A stale offset, a stale state, or a byte that is not a marker all refuse")
func refusals() {
// The state the user saw no longer matches disk an external edit ticked it first.
#expect(BodyMarkup.flippingTaskMarker(in: Self.body, at: 3, expecting: true) == nil)
// Not between brackets at all.
#expect(BodyMarkup.flippingTaskMarker(in: Self.body, at: 7, expecting: false) == nil)
// Past the end, and before the beginning.
#expect(BodyMarkup.flippingTaskMarker(in: Self.body, at: 9_999, expecting: false) == nil)
#expect(BodyMarkup.flippingTaskMarker(in: Self.body, at: 0, expecting: false) == nil)
}
@Test("Multibyte text before the marker does not move it")
func multibyteIsCountedInBytes() {
// "é" is two UTF-8 bytes and "" is three: an offset counted in Characters would be wrong
// by five here, which is exactly the class of bug that puts an `x` in the middle of a word.
let body = "prosé →\n\n- [ ] task\n"
let markup = BodyMarkup.parse(body)
let task = markup.blocks.compactMap { block -> BodyTask? in
if case let .list(list, _) = block { return list.items.first?.task }
return nil
}.first
guard let offset = task?.markerOffset else {
Issue.record("expected a located marker")
return
}
#expect(Array(body.utf8)[offset] == UInt8(ascii: " "))
#expect(BodyMarkup.flippingTaskMarker(in: body, at: offset, expecting: false) == "prosé →\n\n- [x] task\n")
}
}
// MARK: - Source ranges
@Suite("Body markup — source ranges")
struct BodySpanTests {
@Test("A block's range names the bytes it was parsed from")
func blockRangesSlice() {
let source = "# Title\n\nA paragraph.\n"
let blocks = BodyMarkup.parse(source).blocks
#expect(blocks.count == 2)
#expect(slice(source, span(of: blocks.first)) == "# Title")
#expect(slice(source, span(of: blocks.last)) == "A paragraph.")
}
@Test("Ranges are byte offsets, so multibyte text does not shift them")
func rangesAreBytes() {
let source = "# Café\n\n→ next\n"
let blocks = BodyMarkup.parse(source).blocks
#expect(slice(source, span(of: blocks.first)) == "# Café")
#expect(slice(source, span(of: blocks.last)) == "→ next")
}
}
// MARK: - The empty-body question
@Suite("Body markup — emptiness")
struct BodyEmptinessTests {
@Test("Whitespace is empty; anything else is not")
func emptiness() {
// The input to 05's opening rule: "a card opens in Preview unless its body is empty".
#expect(BodyMarkup.isEmpty(""))
#expect(BodyMarkup.isEmpty("\n"))
#expect(BodyMarkup.isEmpty(" \n\n\t"))
#expect(!BodyMarkup.isEmpty("x"))
#expect(!BodyMarkup.isEmpty("\n# Heading\n"))
}
}
// MARK: - The clickable-run codec
@Suite("Body markup — the checkbox link codec")
@MainActor
struct CardBodyLinkTests {
@Test("A checkbox round-trips through its URL, in both states")
func taskURLsRoundTrip() {
for (offset, checked) in [(0, false), (3, true), (12_345, false)] {
guard let url = CardBodyLink.task(offset: offset, isChecked: checked) else {
Issue.record("expected a URL for \(offset)/\(checked)")
continue
}
let parsed = CardBodyLink.parseTask(url)
#expect(parsed?.offset == offset)
#expect(parsed?.isChecked == checked)
}
}
@Test("An ordinary link is not a checkbox")
func ordinaryLinksAreNotTasks() {
// How the delegate tells the two apart a body link must never be mistaken for a write.
#expect(CardBodyLink.parseTask(URL(string: "https://example.com")!) == nil)
#expect(CardBodyLink.parseTask(URL(fileURLWithPath: "/tmp/x.md")) == nil)
}
}
+62
View File
@@ -189,6 +189,68 @@ struct CardWindowMetricsTests {
}
}
// MARK: - The body column's mode
/// 05-card-window.md Mode grammar, as the two rules a unit test can hold: **Preview is the resting
/// state unless the body is empty**, and **the flip is one flip** whichever key produced it.
///
/// Both are silent failures of exactly the kind this file exists for. A card that opened in Preview
/// with an empty body would look like a working window showing nothing, and the user would have to
/// discover E to write the first word of a card they just made which is the ceremony the rule was
/// settled to remove ("a new card has nothing to preview, so during creation flows title body
/// without a mode stop").
@MainActor
@Suite("Card body mode")
struct CardBodyModeTests {
@Test("A card with a body opens in Preview")
func aCardWithABodyOpensInPreview() {
#expect(CardBodyMode.opening(body: "# Notes\n") == .preview)
#expect(CardBodyMode.opening(body: "x") == .preview)
}
@Test("An empty body opens straight into Edit — whitespace included")
func anEmptyBodyOpensInEdit() {
#expect(CardBodyMode.opening(body: "") == .edit)
#expect(CardBodyMode.opening(body: "\n") == .edit)
// A card the app itself just minted has exactly this body: `BoardWriter.newDocumentText`
// writes frontmatter and nothing after the closing delimiter.
#expect(CardBodyMode.opening(body: " \n\t\n") == .edit)
}
@Test("⌘E, Return in Preview and Escape in Edit are one flip")
func theToggleIsSymmetric() {
#expect(CardBodyMode.preview.toggled == .edit)
#expect(CardBodyMode.edit.toggled == .preview)
#expect(CardBodyMode.preview.toggled.toggled == .preview)
}
@Test("The opening rule runs once, not on every snapshot")
func theOpeningRuleIsAppliedOnce() {
let presentation = CardBodyPresentation()
#expect(presentation.openIfNeeded(body: "# Notes\n") == .preview)
// The user presses E
presentation.toggleMode()
#expect(presentation.mode == .edit)
// and a watcher reload arrives with the same body. Re-deciding here would throw the user
// out of the surface they just asked for.
#expect(presentation.openIfNeeded(body: "# Notes\n") == .edit)
#expect(presentation.mode == .edit)
}
@Test("A window that opened into Edit is not dragged back to Preview when the body fills up")
func aLaterBodyDoesNotReopenTheRule() {
let presentation = CardBodyPresentation()
#expect(presentation.openIfNeeded(body: "") == .edit)
// The user types; the reload brings the text back. The rule is about *opening* a card, and
// this window is already open.
#expect(presentation.openIfNeeded(body: "first words") == .edit)
}
}
// MARK: - Identity and frames
@MainActor
+346
View File
@@ -0,0 +1,346 @@
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)
}
}