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 """ } 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", ]) 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", "---\ndupe: 1\ndupe: 2\n---\nbody\n", "---\n\"unterminated: 1\n---\nbody\n", "---\nfoo: {a: 1\n---\nbody\n", "---\n*undefined-alias\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) } } } // 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") } } // 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]")) } } // 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: \"#ff8800\"").background == .valid("#ff8800")) #expect(try document("background: 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)) } @Test func malformedLenientValuesArePreservedVerbatimAndDoNotThrow() throws { #expect(try document("background: [red, blue]").background == .malformed(raw: "[red, blue]")) #expect(try document("background: 42").background == .malformed(raw: "42")) #expect(try document("icon: {a: 1}").icon == .malformed(raw: "{a: 1}")) #expect(try document("iconColor: true").iconColor == .malformed(raw: "true")) #expect(try document("title: [a, b]").title == .malformed(raw: "[a, b]")) } @Test func widthIsLenientForAnythingButAnIntegerAtLeastOne() throws { #expect(try document("width: 0").width == .malformed(raw: "0")) #expect(try document("width: -3").width == .malformed(raw: "-3")) #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) } @Test func malformedLenientValuesStillRoundTrip() throws { let text = "---\nschema: 1\nbackground: [red, blue]\nwidth: 0\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")) } } // 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\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: - 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))])) } }