Files
lanework/KanbanTests/FrontmatterTests.swift
T
rzen bab456c08d Collapsible lanes — frontmatter-backed slim strips outside the width division
A lane folds to a fixed slim vertical strip carrying its glyph, its card-count
badge and its title turned on its side, and the strip is deliberately not part
of the window's division: the expanded lanes' units divide what is left once
each folded strip's fixed width has come off the top, so folding a lane is a
re-divide trigger of the Show/Hide Trash family — the window never moves and
the siblings grow into what the lane gave up.

The state is a first-class lane frontmatter key, `collapsed: true`, and
document state exactly as `width` is: the files are the board, so an agent
folds a lane by writing one key. Absent means expanded, expanding removes the
key rather than writing `false` (the remove-at-default family beside a
one-unit `width`, the empty rename's `title` and the None well's
`background`), and the lane's `width` rides along untouched so expanding
restores the lane the user had. The read is `width`'s leniency one type over —
a boolean scalar or a quoted boolean word reads as itself, everything else has
no reading at all and renders as expanded, bytes preserved either way.

Toggling is the header's always-visible collapse chevron, the lane context
menu's single Collapse Lane / Expand Lane row, and a plain click anywhere on
the strip; a modified click on the strip stays the ordinary selection grammar,
so a folded lane is still selectable by pointer. The title reads bottom-up and
is justified to the top of the room below the strip's chrome (owner ruling
2026-08-08), truncating against the strip's own height.

While folded the lane draws no cards at all, which is what makes every
exclusion true by construction rather than by a guard per gesture: no card
face means no marquee target and no navigation frame, and no registered grid
means the masonry's drop zones have nothing to resolve against. What did need
code is the half that names absolute destinations — the option-arrow jumps and
the arrow seed scan past a folded lane, the lane domain's down-arrow is inert
on one, and New Card skips it (a selection inside one falls through to the
last-active lane, the stale selection's rule). A drop on the strip appends at
the lane's end, cards and Finder files alike, with an accent edge standing in
for the shadow the strip has no masonry to open; there is no hover-to-auto-
expand yet. Lane reorder works on the strip, and a dragged folded lane carries
its fold, so its shadow and its replica are the strip rather than its units.

The write is `writeLaneWidths` clause for clause — one `updateIndex` bracket,
the same stamp behaviour, the same three do-nothing paths — with two new
`WriteOperation` cases and two new undo verbs rather than one of each, because
a banner or an Edit-menu row that said "resize" after Collapse Lane would name
a control the user never touched.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:53:10 -04:00

1181 lines
55 KiB
Swift

import Foundation
import Testing
@testable import Kanban
// MARK: - Fixtures
private enum Fixture {
static let rich = """
---
# board settings, hand-written
schema: 1
title: My Board
project: lanework # agent overlay
sphere: work
labels: [a, b, c]
template: {order: 3}
notes: |
line one
indented two
folded: >
wrapped
text
created: 2026-07-26T16:41:38Z
modified-by: claude
tagged: !!str 42
"quoted key": yes
# trailing note
---
Body text.
More body — with *markdown*.
"""
static let minimal = """
---
schema: 1
order: 1024
title: Thing
---
Body
"""
/// Verbatim copy of a real card written by an agent.
static let realCard = """
---
schema: 1
title: Build the frontmatter engine with byte-perfect round-trip
order: 3072
created: 2026-07-26T16:41:38Z
modified: 2026-07-26T19:06:55Z
modified-by: claude
source: DESIGN/01-storage-format.md
labels: [m1-storage-read]
---
Build the Frontmatter component.
**Design constraints:**
- index.md is YAML frontmatter between `---` delimiters, then a Markdown body.
"""
/// A flow mapping whose continuation sits at column 0 — the shape that would fool a naive
/// "any `key:` at column 0 starts a field" scanner.
static let unindentedFlow = """
---
flow: {a: 1,
b: 2}
last: x
---
body
"""
/// A hand-edited file that says `title` twice — strict YAML would reject it; the editor reads
/// the last one and keeps both on disk (01-storage-format.md § Frontmatter).
static let duplicated = """
---
schema: 1
title: First
order: 1024
title: Second
---
body
"""
}
private func parseError(_ text: String) -> FrontmatterError? {
do {
_ = try FrontmatterDocument.parse(text)
return nil
} catch {
return error
}
}
// MARK: - Round-trip
struct FrontmatterRoundTripTests {
@Test(arguments: [
Fixture.rich,
Fixture.minimal,
Fixture.realCard,
Fixture.unindentedFlow,
// body with a trailing newline
"---\nschema: 1\n---\nbody\n",
// body with several trailing newlines
"---\nschema: 1\n---\nbody\n\n\n",
// no body at all, closing delimiter unterminated
"---\nschema: 1\n---",
// no body, closing delimiter terminated
"---\nschema: 1\n---\n",
// empty frontmatter mapping
"---\n---\nbody\n",
// frontmatter that is only blank lines
"---\n\n\n---\nbody\n",
// frontmatter that is only comments
"---\n# nothing to see\n---\nbody\n",
// explicit empty flow mapping
"---\n{}\n---\nbody\n",
// top-level flow mapping
"---\n{schema: 1, order: 5}\n---\nbody\n",
// odd key order, blank lines between entries
"---\nzzz: last\n\norder: 5\n\n\nschema: 1\n---\nbody\n",
// block scalar keep/strip indicators
"---\nkeep: |+\n text\n\nstrip: |-\n text\n---\nbody\n",
// anchors and aliases
"---\nbase: &b\n x: 1\ncopy: *b\n---\nbody\n",
// quoted values with escapes and embedded delimiters
"---\ntitle: \"a \\\"quoted\\\" --- thing\"\nother: 'single ---'\n---\nbody\n",
// a --- inside a block scalar (indented, so not a delimiter)
"---\nnote: |\n ---\n still the value\n---\nbody\n",
// delimiter line with trailing spaces
"--- \nschema: 1\n--- \nbody\n",
// no trailing newline on the body
"---\nschema: 1\n---\nbody without newline",
// CRLF throughout
"---\r\nschema: 1\r\n---\r\nbody\r\n",
// duplicate top-level keys — every occurrence keeps its own span
Fixture.duplicated,
"---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n",
// a duplicate whose earlier twin owns a comment and a block scalar
"---\nnotes: |\n one\n# about notes\nnotes: two\n---\nbody\n",
])
func serializingAnUntouchedDocumentIsByteIdentical(text: String) throws {
let document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
}
@Test func bodyIsEverythingAfterTheClosingDelimiter() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.body == "Body text.\n\nMore body — with *markdown*.")
}
@Test func emptyBodyIsEmpty() throws {
#expect(try FrontmatterDocument.parse("---\nschema: 1\n---\n").body == "")
#expect(try FrontmatterDocument.parse("---\nschema: 1\n---").body == "")
}
@Test func bodyTrailingNewlinesAreNotNormalized() throws {
let document = try FrontmatterDocument.parse("---\nschema: 1\n---\nbody\n\n\n")
#expect(document.body == "body\n\n\n")
}
}
// MARK: - Malformed input
struct FrontmatterMalformedTests {
@Test func missingOpeningDelimiter() {
#expect(parseError("schema: 1\n---\nbody\n") == .missingOpeningDelimiter)
#expect(parseError("") == .missingOpeningDelimiter)
#expect(parseError("\n---\nschema: 1\n---\n") == .missingOpeningDelimiter)
#expect(parseError("----\nschema: 1\n---\n") == .missingOpeningDelimiter)
#expect(parseError("--- yaml\nschema: 1\n---\n") == .missingOpeningDelimiter)
}
@Test func missingClosingDelimiter() {
#expect(parseError("---\nschema: 1\nbody\n") == .missingClosingDelimiter)
#expect(parseError("---\n") == .missingClosingDelimiter)
#expect(parseError("---") == .missingClosingDelimiter)
}
@Test(arguments: [
"---\nfoo: [1, 2\n---\nbody\n",
"---\nschema: 1\n\tindented-with-tab: 2\n---\nbody\n",
"---\nschema: 1\n bad: indent\n---\nbody\n",
"---\n\"unterminated: 1\n---\nbody\n",
"---\nfoo: {a: 1\n---\nbody\n",
"---\n*undefined-alias\n---\nbody\n",
// last-wins rescues duplicates at the top level only — nested ones stay a hard failure
"---\nouter:\n a: 1\n a: 2\n---\nbody\n",
"---\nflow: {a: 1, a: 2}\n---\nbody\n",
"---\ndupe: 1\ndupe: 2\nouter:\n a: 1\n a: 2\n---\nbody\n",
])
func unparseableYAML(text: String) {
guard case .unparseableYAML = parseError(text) else {
Issue.record("expected .unparseableYAML for \(text.debugDescription), got \(String(describing: parseError(text)))")
return
}
}
@Test func unparseableYAMLCarriesAFileRelativeLineNumber() {
guard case let .unparseableYAML(message, line) = parseError("---\nschema: 1\n bad: indent\n---\n") else {
Issue.record("expected .unparseableYAML")
return
}
#expect(line == 3)
#expect(!message.isEmpty)
}
@Test(arguments: [
"---\n- a\n- b\n---\nbody\n",
"---\njust a scalar\n---\nbody\n",
"---\n42\n---\nbody\n",
"---\n[1, 2]\n---\nbody\n",
])
func frontmatterNotAMapping(text: String) {
#expect(parseError(text) == .frontmatterNotAMapping)
}
@Test(arguments: [
"---\n---\nbody\n",
"---\n\n---\nbody\n",
"---\n# just a comment\n---\nbody\n",
"---\n{}\n---\nbody\n",
"---\nnull\n---\nbody\n",
"---\n~\n---\nbody\n",
])
func emptyFrontmatterIsAnEmptyMapping(text: String) throws {
let document = try FrontmatterDocument.parse(text)
#expect(document.keys.isEmpty)
#expect(document.serialized() == text)
}
@Test func wellFormedButUnusualYAMLIsAccepted() throws {
for text in [Fixture.rich, Fixture.unindentedFlow, "---\nbase: &b\n x: 1\ncopy: *b\n---\n"] {
#expect(parseError(text) == nil)
}
}
/// Strict YAML rejects a repeated mapping key; the editor deliberately does not.
@Test func duplicateTopLevelKeysAreNotAParseError() throws {
for text in [
Fixture.duplicated,
"---\ndupe: 1\ndupe: 2\n---\nbody\n",
"---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n",
] {
#expect(parseError(text) == nil)
}
}
}
// MARK: - Duplicate keys (last one wins)
/// 01-storage-format.md § Frontmatter: a key written twice reads as its last occurrence, the
/// earlier ones preserved verbatim on disk and invisible; an app write of that key rewrites the
/// winner and removes the twins; a removal removes them all.
struct FrontmatterDuplicateKeyTests {
@Test func theLastOccurrenceIsTheOneThatReads() throws {
let document = try FrontmatterDocument.parse(Fixture.duplicated)
#expect(document.title == .valid("Second"))
#expect(document.value(for: "title") == .string("Second"))
#expect(document.rawValue(for: "title") == "Second")
#expect(document.contains("title"))
}
@Test func threeOccurrencesStillReadAsTheLast() throws {
let document = try FrontmatterDocument.parse("---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
#expect(document.title == .valid("c"))
#expect(document.keys == ["title"])
}
/// The effective view has one entry per key, sitting where the winner sits — the order the
/// file itself takes once `set` collapses the twins.
@Test func iterationPresentsTheEffectiveView() throws {
let document = try FrontmatterDocument.parse(Fixture.duplicated)
#expect(document.keys == ["schema", "order", "title"])
#expect(document.fields.map(\.key) == ["schema", "order", "title"])
#expect(document.fields.last?.rawValue == "Second")
}
@Test func bothOccurrencesSurviveAnUntouchedRoundTrip() throws {
let document = try FrontmatterDocument.parse(Fixture.duplicated)
let output = document.serialized()
#expect(output == Fixture.duplicated)
#expect(output.contains("title: First"))
#expect(output.contains("title: Second"))
}
@Test func settingRewritesTheWinnerAndDeletesTheTwins() throws {
var document = try FrontmatterDocument.parse(Fixture.duplicated)
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\nschema: 1\norder: 1024\ntitle: Third\n---\nbody\n")
#expect(document.title == .valid("Third"))
}
@Test func settingCollapsesThreeOccurrencesToOne() throws {
var document = try FrontmatterDocument.parse("---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
document.set("title", to: .string("d"))
#expect(document.serialized() == "---\ntitle: d\n---\nbody\n")
}
/// A stale twin left behind would resurrect itself the moment the winner were removed.
@Test func removingTakesEveryOccurrence() throws {
var document = try FrontmatterDocument.parse(Fixture.duplicated)
document.remove("title")
#expect(document.serialized() == "---\nschema: 1\norder: 1024\n---\nbody\n")
#expect(document.title == .missing)
#expect(!document.contains("title"))
#expect(document.keys == ["schema", "order"])
}
@Test func removingTakesAllThreeOccurrences() throws {
var document = try FrontmatterDocument.parse("---\nschema: 1\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
document.remove("title")
#expect(document.serialized() == "---\nschema: 1\n---\nbody\n")
}
/// Own-line comments outlive the twin they were written above (01-storage-format.md).
@Test func aCommentAboveTheFirstOccurrenceSurvivesTheCollapse() throws {
let text = "---\n# the original title\ntitle: First\ntitle: Second\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\n# the original title\ntitle: Third\n---\nbody\n")
}
/// Same rule when the comment trails the earlier twin rather than heading it.
@Test func aCommentBelowTheFirstOccurrenceSurvivesTheCollapse() throws {
var document = try FrontmatterDocument.parse("---\ntitle: First\n# still relevant\ntitle: Second\n---\nbody\n")
document.remove("title")
#expect(document.serialized() == "---\n# still relevant\n---\nbody\n")
}
@Test func twoDifferentKeysMayEachBeDuplicated() throws {
let text = "---\na: 1\nb: 1\na: 2\nb: 2\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.value(for: "a") == .int(2))
#expect(document.value(for: "b") == .int(2))
#expect(document.keys == ["a", "b"])
document.set("a", to: .int(3))
#expect(document.serialized() == "---\nb: 1\na: 3\nb: 2\n---\nbody\n")
}
/// A duplicated twin whose value is a block scalar still spans its own lines.
@Test func aDuplicateWithAMultiLineTwinCollapsesCleanly() throws {
let text = "---\nnotes: |\n one\n# about notes\nnotes: two\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.value(for: "notes") == .string("two"))
document.set("notes", to: .string("three"))
#expect(document.serialized() == "---\n# about notes\nnotes: three\n---\nbody\n")
}
/// The twins need not be spelled alike — YAML reads `"title"` and `title` as the same key,
/// and each occurrence is rewritten (or dropped) in its own spelling.
@Test func twinsSpelledDifferentlyAreStillTheSameKey() throws {
let text = "---\n\"title\": First\ntitle: Second\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.title == .valid("Second"))
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\ntitle: Third\n---\nbody\n")
}
@Test func duplicatesInACRLFFileRoundTripAndCollapse() throws {
let text = "---\r\ntitle: First\r\norder: 1\r\ntitle: Second\r\n---\r\nbody\r\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.title == .valid("Second"))
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\r\norder: 1\r\ntitle: Third\r\n---\r\nbody\r\n")
}
@Test func duplicatesSurviveReparsing() throws {
var document = try FrontmatterDocument.parse(Fixture.duplicated)
document.set("order", to: .double(2048))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.serialized() == document.serialized())
#expect(reparsed.title == .valid("Second"))
#expect(reparsed.order == .valid(2048))
}
}
// MARK: - Generic access
struct FrontmatterAccessTests {
@Test func keysAreInDocumentOrder() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.keys == [
"schema", "title", "project", "sphere", "labels", "template",
"notes", "folded", "created", "modified-by", "tagged", "quoted key",
])
}
@Test func realCardReadsAsExpected() throws {
let document = try FrontmatterDocument.parse(Fixture.realCard)
#expect(document.schema == .valid(1))
#expect(document.order == .valid(3072))
#expect(document.title == .valid("Build the frontmatter engine with byte-perfect round-trip"))
#expect(document.modifiedBy == .valid("claude"))
#expect(document.created.value != nil)
#expect(document.unknownFields.map(\.key) == ["source", "labels"])
#expect(document.body.hasPrefix("Build the Frontmatter component.\n"))
}
@Test func unknownKeysExcludeSchemaOwnedOnes() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.unknownFields.map(\.key) == [
"project", "sphere", "labels", "template", "notes", "folded", "tagged", "quoted key",
])
}
@Test func modifiedByIsExposedDistinctlyAndIsNotAnUnknownKey() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.modifiedBy == .valid("claude"))
#expect(!document.unknownFields.contains { $0.key == "modified-by" })
#expect(document.contains("modified-by"))
}
@Test func reservedKeysAreOrdinaryUnknownKeys() throws {
let text = """
---
schema: 1
labels: [bug, ui]
assignees: [ann]
due: 2026-08-01
remote: {kind: gitea}
remote-state: synced
template: {order: 2}
---
body
"""
let document = try FrontmatterDocument.parse(text)
#expect(document.unknownFields.map(\.key)
== ["labels", "assignees", "due", "remote", "remote-state", "template"])
#expect(document.serialized() == text)
}
@Test func parsedValuesAreAvailableForUnknownKeys() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.value(for: "labels") == .sequence([.string("a"), .string("b"), .string("c")]))
#expect(document.value(for: "template")
== .mapping([YAMLValue.Pair(key: .string("order"), value: .int(3))]))
#expect(document.value(for: "quoted key") == .bool(true))
#expect(document.value(for: "tagged") == .string("42"))
#expect(document.value(for: "nope") == nil)
}
@Test func rawValuePreservesBlockScalarsVerbatim() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.rawValue(for: "notes") == "|\n line one\n indented two")
#expect(document.rawValue(for: "labels") == "[a, b, c]")
#expect(document.rawValue(for: "schema") == "1")
}
/// A trailing comment belongs to the line, not to the value — reads stop at it.
@Test func rawValueStopsAtAnInlineComment() throws {
let document = try FrontmatterDocument.parse(Fixture.rich)
#expect(document.rawValue(for: "project") == "lanework")
}
/// …but a `#` inside a quoted scalar is value text, and a multi-line value is never scanned.
@Test func rawValueKeepsAHashThatIsNotAComment() throws {
let text = """
---
quoted: "hash # inside"
single: 'hash # inside'
both: "hash # inside" # and a real one
flow: [a, "b # c"] # note
block: |
text # not a comment out here either
---
body
"""
let document = try FrontmatterDocument.parse(text)
#expect(document.rawValue(for: "quoted") == "\"hash # inside\"")
#expect(document.rawValue(for: "single") == "'hash # inside'")
#expect(document.rawValue(for: "both") == "\"hash # inside\"")
#expect(document.rawValue(for: "flow") == "[a, \"b # c\"]")
#expect(document.rawValue(for: "block") == "|\n text # not a comment out here either")
#expect(document.serialized() == text)
}
}
// MARK: - Strict fields
struct FrontmatterStrictFieldTests {
private func document(_ frontmatter: String) throws -> FrontmatterDocument {
try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
}
@Test func schemaValidMissingMalformed() throws {
#expect(try document("schema: 1").schema == .valid(1))
#expect(try document("title: x").schema == .missing)
#expect(try document("schema:").schema == .missing)
#expect(try document("schema: banana").schema == .malformed(raw: "banana"))
#expect(try document("schema: 1.5").schema == .malformed(raw: "1.5"))
#expect(try document("schema: [1]").schema == .malformed(raw: "[1]"))
}
@Test func orderValidMissingMalformed() throws {
#expect(try document("order: 1024").order == .valid(1024))
#expect(try document("order: 1536.5").order == .valid(1536.5))
#expect(try document("order: -1024").order == .valid(-1024))
#expect(try document("schema: 1").order == .missing)
#expect(try document("order:").order == .missing)
}
@Test func nonNumericOrderIsMalformedNotNil() throws {
let malformed = try document("order: banana").order
#expect(malformed == .malformed(raw: "banana"))
#expect(malformed.value == nil)
#expect(malformed.isMalformed)
#expect(!malformed.isMissing)
#expect(malformed.rawText == "banana")
#expect(try document("order: \"1024\"").order == .malformed(raw: "\"1024\""))
#expect(try document("order: [1]").order == .malformed(raw: "[1]"))
}
/// NaN has no place in the total order the tie-break and midpoint math assume
/// (01-storage-format.md § Frontmatter, settled): a non-finite reading is `.malformed` exactly
/// like a non-numeric one, never a silently `.valid(Double.nan)`.
///
/// What that *costs* is the rulebook's, and it changed on 2026-07-31: below the board root both
/// shapes now read as append-at-end, coerce-tier, rather than failing the load
/// (`IntegrityRules.resolvedOrder`, `BoardLoaderOptionalOrderTests`). The document's reading is
/// unchanged, which is the point of the split.
@Test func nonFiniteOrderIsMalformedNotValid() throws {
#expect(try document("order: .nan").order == .malformed(raw: ".nan"))
#expect(try document("order: .inf").order == .malformed(raw: ".inf"))
#expect(try document("order: +.inf").order == .malformed(raw: "+.inf"))
#expect(try document("order: -.inf").order == .malformed(raw: "-.inf"))
}
}
// MARK: - Lenient fields
struct FrontmatterLenientFieldTests {
private func document(_ frontmatter: String) throws -> FrontmatterDocument {
try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
}
@Test func wellFormedLenientValues() throws {
#expect(try document("title: My Board").title == .valid("My Board"))
#expect(try document("background: {color: \"#ff8800\"}").background == .valid("#ff8800"))
#expect(try document("background: {color: slate}").background == .valid("slate"))
#expect(try document("icon: tray.full").icon == .valid("tray.full"))
#expect(try document("iconColor: teal").iconColor == .valid("teal"))
#expect(try document("width: 3").width == .valid(3))
#expect(try document("width: 1").width == .valid(1))
}
/// A scalar of the wrong YAML type still has a sensible string reading — it coerces to the
/// source text the author typed (01-storage-format.md § Frontmatter). Only a sequence or
/// mapping — no scalar to read at all — is malformed.
@Test func scalarsOfTheWrongTypeCoerceToTheirSourceText() throws {
#expect(try document("title: 2048").title == .valid("2048"))
#expect(try document("title: true").title == .valid("true"))
#expect(try document("background: {color: 42}").background == .valid("42"))
#expect(try document("iconColor: true").iconColor == .valid("true"))
#expect(try document("icon: 2026-07-26T16:41:38Z").icon == .valid("2026-07-26T16:41:38Z"))
}
/// The comment on a value line belongs to the line, not to the value: the source text a
/// wrong-typed scalar coerces to stops where the comment starts.
@Test func aTrailingCommentIsNotPartOfACoercedValue() throws {
#expect(try document("title: 2048 # note").title == .valid("2048"))
#expect(try document("title: true # note").title == .valid("true"))
#expect(try document("background: {color: 42}\t# tabbed").background == .valid("42"))
#expect(try document("icon: 2026-07-26T16:41:38Z # when").icon == .valid("2026-07-26T16:41:38Z"))
}
/// A `#` inside a quoted scalar is value text — the quotes are what make `#ff8800` writable
/// at all, so a read must not treat it as a comment.
@Test func aHashInsideAQuotedValueIsNotTrimmed() throws {
#expect(try document("title: \"2048 # note\"").title == .valid("2048 # note"))
#expect(try document("background: {color: \"#ff8800\"}").background == .valid("#ff8800"))
#expect(try document("background: {color: \"#ff8800\"} # brand orange").background == .valid("#ff8800"))
}
/// A sequence or mapping has no scalar reading at all; the raw text it falls back to stops at
/// the comment like every other read.
@Test func malformedRawStopsAtAnInlineComment() throws {
#expect(try document("background: [red, blue] # a palette").background == .malformed(raw: "[red, blue]"))
#expect(try document("title: {a: 1} # a mapping").title == .malformed(raw: "{a: 1}"))
#expect(try document("width: wide # roughly").width == .malformed(raw: "wide"))
#expect(try document("order: banana # not a number").order == .malformed(raw: "banana"))
}
@Test func quotedStringLenientValuesAreUnaffectedByCoercion() throws {
#expect(try document("title: \"2048\"").title == .valid("2048"))
#expect(try document("title: \"true\"").title == .valid("true"))
#expect(try document("title: My Board").title == .valid("My Board"))
}
@Test func malformedLenientValuesArePreservedVerbatimAndDoNotThrow() throws {
#expect(try document("background: [red, blue]").background == .malformed(raw: "[red, blue]"))
#expect(try document("icon: {a: 1}").icon == .malformed(raw: "{a: 1}"))
#expect(try document("title: [a, b]").title == .malformed(raw: "[a, b]"))
}
// MARK: `background` is a mapping and only a mapping
/// **The retired scalar** (01-storage-format.md § Frontmatter, ruled 2026-08-06 before anything
/// shipped — one shape, no legacy spelling, no migration): `background: green` has no reading at
/// all. It is `.malformed` like any other unreadable value, which renders as no colour and
/// leaves the bytes exactly as written.
@Test func aScalarBackgroundHasNoReading() throws {
#expect(try document("background: green").background == .malformed(raw: "green"))
#expect(try document("background: \"#ff8800\"").background == .malformed(raw: "\"#ff8800\""))
#expect(try document("background: 42").background == .malformed(raw: "42"))
// The raw stops at the comment like every other read, since a comment is the line's.
#expect(try document("background: green # my colour").background == .malformed(raw: "green"))
}
/// One unreadable value is reported **once**: the colour reading owns the key's shape, and the
/// image stays silent about a file that never wrote a mapping to name a picture in.
@Test func onlyTheColourReportsANonMappingShape() throws {
#expect(try document("background: green").backgroundImage == .missing)
#expect(try document("background: [red, blue]").backgroundImage == .missing)
#expect(try document("background: 42").backgroundImage == .missing)
#expect(try document("schema: 1").backgroundImage == .missing)
#expect(try document("background: green").coercedFields
== [CoercedField(key: "background", raw: "green")])
}
/// The mapping form, both halves present — flow and block spellings are one YAML value and
/// therefore one reading.
@Test func aMappingBackgroundReadsBothSubkeys() throws {
let flow = try document("background: {color: \"#112233\", image: sunset.jpg}")
#expect(flow.background == .valid("#112233"))
#expect(flow.backgroundImage == .valid("sunset.jpg"))
let block = try FrontmatterDocument.parse(
"---\nbackground:\n color: fern\n image: art/sunset.jpg\n---\nbody\n"
)
#expect(block.background == .valid("fern"))
#expect(block.backgroundImage == .valid("art/sunset.jpg"))
}
/// Either half may be absent, and an absent half is `.missing` — not malformed. An image-only
/// background is a board with no colour, which is the level's default and not a fallback.
@Test func eitherSubkeyMayBeAbsent() throws {
let colorOnly = try document("background: {color: chalk}")
#expect(colorOnly.background == .valid("chalk"))
#expect(colorOnly.backgroundImage == .missing)
let imageOnly = try document("background: {image: sunset.jpg}")
#expect(imageOnly.background == .missing)
#expect(imageOnly.backgroundImage == .valid("sunset.jpg"))
let empty = try document("background: {}")
#expect(empty.background == .missing)
#expect(empty.backgroundImage == .missing)
}
/// An explicit null subkey reads exactly like an absent one — `FieldValue.missing` already
/// treats `background: null` that way, and a subkey is no different.
@Test func nullSubkeysReadAsMissing() throws {
let nulls = try document("background: {color: null, image: ~}")
#expect(nulls.background == .missing)
#expect(nulls.backgroundImage == .missing)
}
/// Unknown subkeys are tolerated on the way in exactly as unknown *keys* are — they mean
/// nothing to either reading and cost it nothing.
@Test func unknownSubkeysAreTolerated() throws {
let extra = try document("background: {opacity: 0.5, color: fern, blend: multiply}")
#expect(extra.background == .valid("fern"))
#expect(extra.backgroundImage == .missing)
}
/// Inside the mapping the subvalues coerce like any other scalar, quoted or not; a subvalue with
/// no scalar reading at all is malformed, quoting the subvalue rather than the whole span —
/// a subkey has no source span of its own to quote.
@Test func subkeyScalarsCoerceAndCollectionsAreMalformed() throws {
#expect(try document("background: {color: 42}").background == .valid("42"))
#expect(try document("background: {image: \"sun set.jpg\"}").backgroundImage == .valid("sun set.jpg"))
#expect(try document("background: {color: [a, b]}").background == .malformed(raw: "[a, b]"))
#expect(try document("background: {image: {a: 1}}").backgroundImage == .malformed(raw: "{a: 1}"))
// The other half of a mapping with one bad subkey still reads perfectly well.
#expect(try document("background: {color: [a, b], image: sunset.jpg}").backgroundImage == .valid("sunset.jpg"))
}
/// **The coerce tier's trace covers both readings** (01-storage-format.md § Frontmatter: "every
/// silent recovery leaves a trace"). Both are filed under the key the schema spells, and are told
/// apart by the subvalue each quotes — so a mapping whose colour reads fine and whose image does
/// not is still visible.
@Test func bothBackgroundReadingsReachTheCoerceRecord() throws {
#expect(try document("background: {color: fern, image: [a, b]}").coercedFields
== [CoercedField(key: "background", raw: "[a, b]")])
#expect(try document("background: {color: [a], image: [b]}").coercedFields
== [CoercedField(key: "background", raw: "[a]"), CoercedField(key: "background", raw: "[b]")])
// A sequence is one unreadable value and is reported once — the image reading stays silent
// about a shape that never claimed to name one.
#expect(try document("background: [red, blue]").coercedFields
== [CoercedField(key: "background", raw: "[red, blue]")])
#expect(try document("background: {image: sunset.jpg}").coercedFields.isEmpty)
}
/// The engine reads the shape; it never rewrites it. A mapping background round-trips
/// byte-identically like every other value the app did not touch.
@Test func aMappingBackgroundRoundTripsVerbatim() throws {
let text = "---\nschema: 1\nbackground:\n color: fern\n image: art/sunset.jpg\n blend: multiply\n---\nbody\n"
let document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.background == .valid("fern"))
#expect(document.backgroundImage == .valid("art/sunset.jpg"))
}
/// Only a fractional or non-numeric reading has no sensible width at all — a sequence,
/// mapping, or scalar with no integer reading whatsoever stays malformed and renders as the
/// default 1.
@Test func widthIsMalformedOnlyForFractionalOrNonNumericReadings() throws {
#expect(try document("width: 1.5").width == .malformed(raw: "1.5"))
#expect(try document("width: wide").width == .malformed(raw: "wide"))
#expect(try document("schema: 1").width == .missing)
}
/// **Ranges are part of the sensible reading** (01-storage-format.md § Frontmatter, settled):
/// an exact-integer reading below 1 — from an int, a double, or a numeric string — coerces to
/// 1 rather than falling back to malformed. The table's "≥ 1" is a validity bound on the
/// coerced value, not a gate on which readings count as sensible.
@Test func anIntegerWidthBelowOneCoercesToOne() throws {
#expect(try document("width: 0").width == .valid(1))
#expect(try document("width: -3").width == .valid(1))
#expect(try document("width: \"0\"").width == .valid(1))
#expect(try document("width: \"-3\"").width == .valid(1))
#expect(try document("width: -1.0").width == .valid(1))
}
/// A string or double with an exact integer reading coerces — below 1 to 1 (see
/// `anIntegerWidthBelowOneCoercesToOne`), at or above 1 to itself; a fractional reading or
/// non-numeric text still has none.
@Test func widthCoercesStringsAndWholeNumberDoubles() throws {
#expect(try document("width: \"2\"").width == .valid(2))
#expect(try document("width: 2.0").width == .valid(2))
#expect(try document("width: 2.7").width == .malformed(raw: "2.7"))
#expect(try document("width: banana").width == .malformed(raw: "banana"))
}
/// **`collapsed` reads like `width`, one type over** (03-board-ui.md § Lane ▸ Collapsed lanes;
/// 01-storage-format.md § Frontmatter's lane table): a boolean scalar reads as itself — including
/// YAML 1.1's other spellings, which the parser has already resolved — and a *quoted* one of those
/// words coerces, the numeric-string precedent applied to a boolean field.
@Test func collapsedReadsBooleansAndTheirQuotedSpellings() throws {
#expect(try document("collapsed: true").collapsed == .valid(true))
#expect(try document("collapsed: false").collapsed == .valid(false))
// YAML 1.1 resolves these to booleans unquoted, so they arrive here already typed.
#expect(try document("collapsed: yes").collapsed == .valid(true))
#expect(try document("collapsed: off").collapsed == .valid(false))
// Quoted, they are strings — and a string that spells a boolean word has a sensible reading.
#expect(try document("collapsed: \"true\"").collapsed == .valid(true))
#expect(try document("collapsed: \"No\"").collapsed == .valid(false))
#expect(try document("collapsed: \"ON\"").collapsed == .valid(true))
}
/// **Everything else has no boolean reading, and therefore renders as expanded** — the default is
/// the absent key's, so an unreadable value can never fold a lane by accident.
@Test func collapsedIsMalformedForEveryNonBooleanReading() throws {
#expect(try document("schema: 1").collapsed == .missing)
#expect(try document("collapsed: null").collapsed == .missing)
#expect(try document("collapsed: maybe").collapsed == .malformed(raw: "maybe"))
#expect(try document("collapsed: 1").collapsed == .malformed(raw: "1"))
#expect(try document("collapsed: [true]").collapsed == .malformed(raw: "[true]"))
#expect(try document("collapsed: {a: 1}").collapsed == .malformed(raw: "{a: 1}"))
}
/// A lenient field with no reading files a coerce-tier trace and leaves the bytes exactly as
/// written — the family's posture, `collapsed` included.
@Test func anUnreadableCollapsedFilesATraceAndRoundTrips() throws {
let text = "---\nschema: 1\ncollapsed: maybe\nwidth: 2\n---\nbody\n"
let parsed = try FrontmatterDocument.parse(text)
#expect(parsed.serialized() == text)
#expect(parsed.coercedFields == [CoercedField(key: "collapsed", raw: "maybe")])
// A readable value is an absence of trace, not a trace of a value.
#expect(try document("collapsed: true").coercedFields.isEmpty)
#expect(try document("collapsed: false").coercedFields.isEmpty)
}
/// The key is the schema's, so the card window's Details section does not list it beside a user's
/// own overlay keys (`FrontmatterKeys.schemaOwned`).
@Test func collapsedIsSchemaOwnedRatherThanAnUnknownKey() throws {
#expect(FrontmatterKeys.schemaOwned.contains(FrontmatterKeys.collapsed))
#expect(try document("collapsed: true").unknownFields.isEmpty)
}
@Test func malformedLenientValuesStillRoundTrip() throws {
let text = "---\nschema: 1\nbackground: [red, blue]\nwidth: 1.5\nicon: {a: 1}\n---\nbody\n"
let document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.background.isMalformed)
#expect(document.width.isMalformed)
#expect(document.icon.isMalformed)
}
@Test func timestampsReadPlainAndQuoted() throws {
let expected = Date(timeIntervalSince1970: 1_785_084_098) // 2026-07-26T16:41:38Z
#expect(try document("created: 2026-07-26T16:41:38Z").created == .valid(expected))
#expect(try document("modified: \"2026-07-26T16:41:38Z\"").modified == .valid(expected))
#expect(try document("deleted: 2026-07-26T16:41:38Z").deleted == .valid(expected))
#expect(try document("schema: 1").deleted == .missing)
#expect(try document("created: never").created == .malformed(raw: "never"))
}
/// A quoted ISO-8601 string is not YAML's implicit timestamp type — it parses as `.string`
/// — but still coerces to a valid date (01-storage-format.md § Frontmatter).
@Test func quotedISO8601StringCoercesToAValidDate() throws {
let expected = Date(timeIntervalSince1970: 1_767_323_045) // 2026-01-02T03:04:05Z
#expect(try document("created: \"2026-01-02T03:04:05Z\"").created == .valid(expected))
}
}
// MARK: - Surgical edits
struct FrontmatterEditTests {
@Test func settingOneKeyLeavesEveryOtherByteIdentical() throws {
var document = try FrontmatterDocument.parse(Fixture.rich)
document.set("title", to: .string("Renamed"))
let output = document.serialized()
let expected = Fixture.rich.replacingOccurrences(of: "title: My Board", with: "title: Renamed")
#expect(output == expected)
// Prefix and suffix around the edited span are untouched.
let marker = "title: "
let originalPrefix = Fixture.rich.prefix(while: { _ in true }).components(separatedBy: marker)[0]
#expect(output.hasPrefix(originalPrefix))
#expect(output.hasSuffix("\nMore body — with *markdown*."))
}
@Test func settingAKeyPreservesItsTrailingCommentsAndBlankLines() throws {
let text = "---\nschema: 1\nproject: lanework # agent overlay\n\n# a note\norder: 5\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
document.set("project", to: .string("kanban"))
#expect(document.serialized()
== "---\nschema: 1\nproject: kanban # agent overlay\n\n# a note\norder: 5\n---\nbody\n")
}
@Test func appendingANewKeyGoesBeforeTheClosingDelimiter() throws {
var document = try FrontmatterDocument.parse(Fixture.minimal)
document.set("modified", to: .date(Date(timeIntervalSince1970: 1_785_084_098)))
#expect(document.serialized() == """
---
schema: 1
order: 1024
title: Thing
modified: 2026-07-26T16:41:38Z
---
Body
""")
#expect(document.keys == ["schema", "order", "title", "modified"])
}
@Test func appendingToEmptyFrontmatter() throws {
var document = try FrontmatterDocument.parse("---\n---\nbody\n")
document.set("schema", to: .int(1))
#expect(document.serialized() == "---\nschema: 1\n---\nbody\n")
#expect(document.schema == .valid(1))
}
@Test func removingAKeyDeletesOnlyItsLines() throws {
var document = try FrontmatterDocument.parse(Fixture.minimal)
document.remove("order")
#expect(document.serialized() == "---\nschema: 1\ntitle: Thing\n---\nBody")
#expect(document.order == .missing)
#expect(document.keys == ["schema", "title"])
}
@Test func removingAKeyKeepsTrailingComments() throws {
var document = try FrontmatterDocument.parse("---\na: 1\n# note about b\nb: 2\n---\nbody\n")
document.remove("a")
#expect(document.serialized() == "---\n# note about b\nb: 2\n---\nbody\n")
}
@Test func removingAnAbsentKeyIsANoOp() throws {
var document = try FrontmatterDocument.parse(Fixture.minimal)
document.remove("nope")
#expect(document.serialized() == Fixture.minimal)
}
@Test func editingAKeyWhoseValueWasABlockScalar() throws {
var document = try FrontmatterDocument.parse(Fixture.rich)
document.set("notes", to: .string("flattened"))
#expect(document.serialized().contains("notes: flattened\nfolded: >\n"))
#expect(!document.serialized().contains("line one"))
#expect(document.value(for: "notes") == .string("flattened"))
// Everything after the edited span is untouched.
#expect(document.serialized().hasSuffix("More body — with *markdown*."))
}
@Test func editingAKeyAfterAnUnindentedFlowContinuation() throws {
var document = try FrontmatterDocument.parse(Fixture.unindentedFlow)
document.set("last", to: .string("y"))
#expect(document.serialized() == "---\nflow: {a: 1,\nb: 2}\nlast: y\n---\nbody")
}
@Test func editingAQuotedKeyKeepsItsOriginalSpelling() throws {
var document = try FrontmatterDocument.parse("---\n\"quoted key\": one\n---\nbody\n")
document.set("quoted key", to: .string("two"))
#expect(document.serialized() == "---\n\"quoted key\": two\n---\nbody\n")
}
@Test func buildingADocumentFromScratch() throws {
var document = FrontmatterDocument(body: "The card body.\n")
document.set("schema", to: .int(1))
document.set("order", to: .double(1024))
document.set("title", to: .string("New card"))
let text = document.serialized()
#expect(text == "---\nschema: 1\norder: 1024\ntitle: New card\n---\nThe card body.\n")
#expect(try FrontmatterDocument.parse(text).serialized() == text)
}
@Test func settingTheBodyReplacesOnlyTheBody() throws {
var document = try FrontmatterDocument.parse(Fixture.rich)
document.body = "Brand new body.\n"
let output = document.serialized()
#expect(output.hasSuffix("---\nBrand new body.\n"))
#expect(output.hasPrefix("---\n# board settings, hand-written\nschema: 1\n"))
#expect(!output.contains("More body"))
}
@Test func editsSurviveReparsing() throws {
var document = try FrontmatterDocument.parse(Fixture.rich)
document.set("order", to: .double(2048))
document.remove("modified-by")
document.set("title", to: .string("Renamed"))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.order == .valid(2048))
#expect(reparsed.modifiedBy == .missing)
#expect(reparsed.title == .valid("Renamed"))
#expect(reparsed.unknownFields.map(\.key)
== ["project", "sphere", "labels", "template", "notes", "folded", "tagged", "quoted key"])
#expect(reparsed.serialized() == document.serialized())
}
}
// MARK: - Inline comments on rewritten lines
/// 01-storage-format.md § Frontmatter, "App rewrites preserve comments": an inline comment on a
/// rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain
/// single-line scalars, allowed to drop in pathological shapes.
struct FrontmatterInlineCommentTests {
private func edited(_ frontmatter: String, _ key: String, _ value: FrontmatterValue) throws -> String {
var document = try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
document.set(key, to: value)
return String(document.serialized().dropFirst("---\n".count).dropLast("\n---\nbody\n".count))
}
@Test func aPlainScalarKeepsItsCommentAndItsSpacing() throws {
#expect(try edited("order: 3072 # keep at top", "order", .double(4096))
== "order: 4096 # keep at top")
#expect(try edited("order: 3072 # tight", "order", .double(4096)) == "order: 4096 # tight")
#expect(try edited("order: 3072\t# tabbed", "order", .double(4096)) == "order: 4096\t# tabbed")
}
@Test func aValueLineWithoutACommentIsRewrittenAsBefore() throws {
#expect(try edited("order: 3072", "order", .double(4096)) == "order: 4096")
#expect(try edited("title: My Board", "title", .string("Renamed")) == "title: Renamed")
}
/// A `#` inside a quoted scalar is value text, not a comment — it must not be re-spliced.
@Test func aHashInsideAQuotedScalarIsNotAComment() throws {
#expect(try edited("title: \"hash # inside\"", "title", .string("Renamed")) == "title: Renamed")
#expect(try edited("title: 'hash # inside'", "title", .string("Renamed")) == "title: Renamed")
#expect(try edited("background: \"#ff8800\"", "background", .string("slate")) == "background: slate")
}
/// …but a real comment *after* such a value still is one.
@Test func aCommentAfterAQuotedScalarIsStillSpliced() throws {
#expect(try edited("title: \"hash # inside\" # real note", "title", .string("Renamed"))
== "title: Renamed # real note")
#expect(try edited("title: 'it''s # fine' # real note", "title", .string("Renamed"))
== "title: Renamed # real note")
}
/// An apostrophe mid-scalar does not open a quoted scalar, so the comment after it is found.
@Test func aPlainScalarWithAnApostropheKeepsItsComment() throws {
#expect(try edited("title: it's fine # note", "title", .string("Renamed"))
== "title: Renamed # note")
#expect(try edited("title: say \"hi\" # note", "title", .string("Renamed"))
== "title: Renamed # note")
}
@Test func aSingleLineFlowCollectionKeepsItsComment() throws {
#expect(try edited("labels: [a, \"b # c\"] # note", "labels", .raw("[x]"))
== "labels: [x] # note")
#expect(try edited("template: {order: 3} # picker slot", "template", .raw("{order: 7}"))
== "template: {order: 7} # picker slot")
}
@Test func aCommentOnAnEmptyValueIsKept() throws {
#expect(try edited("title: # to be filled in", "title", .string("Named"))
== "title: Named # to be filled in")
}
@Test func theCommentSurvivesSuccessiveSets() throws {
var document = try FrontmatterDocument.parse("---\norder: 3072 # keep at top\n---\nbody\n")
document.set("order", to: .double(4096))
document.set("order", to: .double(8192))
#expect(document.serialized() == "---\norder: 8192 # keep at top\n---\nbody\n")
}
/// Reading a value and writing it straight back must leave one comment, not two — the read
/// stops at the comment, the write re-splices it.
@Test func aReadThenWriteDoesNotCompoundTheComment() throws {
var document = try FrontmatterDocument.parse("---\ntitle: 2048 # note\n---\nbody\n")
#expect(document.title == .valid("2048"))
document.set("title", to: .string(document.title.value ?? ""))
#expect(document.serialized() == "---\ntitle: \"2048\" # note\n---\nbody\n")
document.set("title", to: .string(document.title.value ?? ""))
#expect(document.serialized() == "---\ntitle: \"2048\" # note\n---\nbody\n")
#expect(try FrontmatterDocument.parse(document.serialized()).title == .valid("2048"))
}
@Test func theCommentSurvivesAReparse() throws {
var document = try FrontmatterDocument.parse("---\norder: 3072 # keep at top\n---\nbody\n")
document.set("order", to: .double(4096))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.order == .valid(4096))
#expect(reparsed.serialized() == document.serialized())
}
/// The winning occurrence's own comment is what carries over; a collapsed twin's inline
/// comment goes with the twin.
@Test func theWinningOccurrencesCommentIsTheOneKept() throws {
var document = try FrontmatterDocument.parse("---\norder: 1 # old\norder: 2 # current\n---\nbody\n")
document.set("order", to: .double(3))
#expect(document.serialized() == "---\norder: 3 # current\n---\nbody\n")
}
/// Pathological shapes are allowed to lose the comment rather than have the editor guess.
/// Pinning the chosen behavior: a value spanning several lines is not scanned at all.
@Test func aMultiLineValueDropsItsHeaderComment() throws {
var document = try FrontmatterDocument.parse("---\nnotes: | # about the notes\n line one\n---\nbody\n")
document.set("notes", to: .string("flattened"))
#expect(document.serialized() == "---\nnotes: flattened\n---\nbody\n")
var flow = try FrontmatterDocument.parse("---\nflow: {a: 1,\nb: 2} # spread out\nlast: x\n---\nbody\n")
flow.set("flow", to: .raw("{a: 9}"))
#expect(flow.serialized() == "---\nflow: {a: 9}\nlast: x\n---\nbody\n")
}
/// Comments on their own lines were always preserved and still are — including the one that
/// heads the frontmatter and the one that trails it.
@Test func ownLineCommentsAreUntouchedByARewrite() throws {
var document = try FrontmatterDocument.parse(Fixture.rich)
document.set("title", to: .string("Renamed"))
let output = document.serialized()
#expect(output.hasPrefix("---\n# board settings, hand-written\nschema: 1\n"))
#expect(output.contains("\n# trailing note\n---\n"))
#expect(output.contains("project: lanework # agent overlay\n"))
}
}
// MARK: - Scalar emission
struct FrontmatterEmissionTests {
private func emittedTitle(_ value: FrontmatterValue) throws -> String {
var document = try FrontmatterDocument.parse("---\n---\n")
document.set("title", to: value)
return String(document.serialized().dropFirst("---\ntitle: ".count).dropLast("\n---\n".count))
}
@Test func numbersAndTimestampsAreEmittedPlain() throws {
#expect(try emittedTitle(.int(42)) == "42")
#expect(try emittedTitle(.double(1024)) == "1024")
#expect(try emittedTitle(.double(1536.5)) == "1536.5")
#expect(try emittedTitle(.bool(true)) == "true")
#expect(try emittedTitle(.date(Date(timeIntervalSince1970: 1_785_084_098))) == "2026-07-26T16:41:38Z")
}
@Test func ordinaryStringsAreEmittedPlain() throws {
#expect(try emittedTitle(.string("My Board")) == "My Board")
#expect(try emittedTitle(.string("Fix login — round 2")) == "Fix login — round 2")
#expect(try emittedTitle(.string("a/b?c")) == "a/b?c")
}
@Test(arguments: [
"",
" padded ",
"true",
"yes",
"null",
"~",
"42",
"1.5",
"2026-07-26T16:41:38Z",
"has: a colon",
"trailing colon:",
"hash # comment",
"#ff8800",
"- leading dash",
"[bracketed]",
"{braced}",
"line\nbreak",
"tab\there",
"quote\"inside",
"back\\slash",
"*anchor",
"&anchor",
"!tagged",
"%directive",
"@at",
"|pipe",
">gt",
"'single'",
])
func stringsNeedingQuotingSurviveAReparse(value: String) throws {
var document = try FrontmatterDocument.parse("---\n---\n")
document.set("title", to: .string(value))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.title == .valid(value))
}
@Test func quotingIsUsedOnlyWhenNeeded() throws {
#expect(try emittedTitle(.string("true")) == "\"true\"")
#expect(try emittedTitle(.string("#ff8800")) == "\"#ff8800\"")
#expect(try emittedTitle(.string("")) == "\"\"")
#expect(try emittedTitle(.string("a\nb")) == "\"a\\nb\"")
// An interior quote is legal in a plain scalar; a leading one is not.
#expect(try emittedTitle(.string("say \"hi\"")) == "say \"hi\"")
#expect(try emittedTitle(.string("\"quoted\"")) == "\"\\\"quoted\\\"\"")
#expect(try emittedTitle(.string("back\\slash")) == "back\\slash")
}
@Test func rawValuesAreWrittenVerbatim() throws {
var document = try FrontmatterDocument.parse("---\nschema: 1\n---\nbody\n")
document.set("template", to: .raw("{order: 7}"))
#expect(document.serialized() == "---\nschema: 1\ntemplate: {order: 7}\n---\nbody\n")
#expect(document.value(for: "template")
== .mapping([YAMLValue.Pair(key: .string("order"), value: .int(7))]))
}
}
// MARK: - Appended lines and empty-but-not-blank blocks
struct FrontmatterAppendedLineTests {
@Test func appendedKeyAdoptsCRLFInACRLFFile() throws {
var document = try FrontmatterDocument.parse("---\r\nschema: 1\r\n---\r\nbody\r\n")
document.set("title", to: .string("x"))
#expect(document.serialized() == "---\r\nschema: 1\r\ntitle: x\r\n---\r\nbody\r\n")
}
@Test func appendedKeyStaysLFInAnLFFile() throws {
var document = try FrontmatterDocument.parse("---\nschema: 1\n---\nbody\n")
document.set("title", to: .string("x"))
#expect(document.serialized() == "---\nschema: 1\ntitle: x\n---\nbody\n")
}
/// `{}`, `null`, `~` resolve to an empty mapping yet leave text no key owns — appending
/// after it would be unparseable YAML, so the shape refuses edits (readable-but-uneditable).
@Test(arguments: ["---\n{}\n---\nbody\n", "---\nnull\n---\nbody\n", "---\n~\n---\nbody\n"])
func emptyButNotBlankFrontmatterIsUneditable(text: String) throws {
#expect(try FrontmatterDocument.parse(text).uneditableShape == .keyWithoutOwnLine)
}
/// Truly blank or comment-only frontmatter stays editable — appending after it is exactly
/// what `set` is for, and the result must reparse.
@Test(arguments: ["---\n---\nbody\n", "---\n\n---\nbody\n", "---\n# just a comment\n---\nbody\n"])
func blankOrCommentOnlyFrontmatterStaysEditable(text: String) throws {
var document = try FrontmatterDocument.parse(text)
#expect(document.uneditableShape == nil)
document.set("schema", to: .int(1))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.schema == .valid(1))
}
}