import Foundation import Testing @testable import Kanban // Golden fixture-board suite (01-storage-format.md, 02-architecture.md § Testing): real // on-disk folder trees under `Fixtures/`, one board per tolerated/valid case and one per // fail-fast case, asserted against `BoardLoader`. Complements `BoardLoaderTests.swift`'s // synthetic smoke coverage — this suite is the comprehensive, hand-authored, disk-backed // counterpart. // MARK: - Bundle resource resolution /// A tiny anchor class purely so `Bundle(for:)` can find the test bundle — there is no /// `Bundle.module` in an xcodeproj target (that's an SPM-only convenience). private final class FixtureBundleAnchor {} /// The `Fixtures/` folder reference, copied into the test bundle's resources verbatim /// (`project.yml`'s `KanbanTests` target). Real directories on disk, not synthesized strings. private func fixturesRoot() -> URL { guard let resources = Bundle(for: FixtureBundleAnchor.self).resourceURL else { fatalError("test bundle has no resourceURL") } return resources.appendingPathComponent("Fixtures", isDirectory: true) } private func fixtureBoard(_ relativePath: String) -> URL { fixturesRoot().appendingPathComponent(relativePath, isDirectory: true) } private func loadFixture(_ relativePath: String) throws -> LoadResult { try BoardLoader.load(boardRoot: fixtureBoard(relativePath)) } /// Every `index.md` beneath `root`, found by walking the real tree — used by the round-trip /// assertions, which don't want to hardcode which files exist. private func allIndexMdFiles(under root: URL) throws -> [URL] { guard let enumerator = FileManager.default.enumerator( at: root, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles] ) else { return [] } var results: [URL] = [] for case let url as URL in enumerator where url.lastPathComponent == "index.md" { results.append(url) } return results } private func iso8601(_ text: String) -> Date { guard let date = ISO8601DateFormatter().date(from: text) else { fatalError("bad test fixture: '\(text)' is not ISO-8601") } return date } private func expectFixtureFailure( _ relativePath: String, path: String, reasonDescription: String, _ matches: (BoardLoadError.Reason) -> Bool ) { do { _ = try loadFixture(relativePath) Issue.record("expected \(relativePath) to fail with \(reasonDescription) at '\(path)', but it loaded") } catch let failure as BoardLoadFailure { // Each `Malformed/` board is minimal — "one broken thing" — so the aggregate holds exactly // one defect, and asserting the count is what keeps that authoring rule true. #expect(failure.defects.count == 1, "\(relativePath): expected one defect, got \(failure.defects)") let error = failure.primary #expect(error.path == path, "\(relativePath): wrong path in error") #expect(matches(error.reason), "\(relativePath): expected \(reasonDescription), got \(error.reason)") } catch { Issue.record("\(relativePath): expected a BoardLoadFailure, got \(error)") } } // MARK: - Valid/rich-board.kanban private enum RichBoard { static let laneDoing = "10000000-0000-4000-8000-000000000001" static let laneDone = "20000000-0000-4000-8000-000000000002" static let cardTaxonomy = "30000000-0000-4000-8000-000000000003" static let cardSecond = "40000000-0000-4000-8000-000000000004" static let cardShip = "50000000-0000-4000-8000-000000000005" } struct FixtureRichBoardTests { @Test func loadsFullShapeWithStylingAndNoWarnings() throws { let result = try loadFixture("Valid/rich-board.kanban") let model = result.model #expect(model.title.value == "Rich Demo Board") #expect(model.background.value == "#1E1E1E") #expect(model.icon.value == "rectangle.stack.fill") #expect(model.iconColor.value == "purple") #expect(model.modifiedBy.value == "claude") if case let .mapping(pairs) = model.template { #expect(pairs.count == 1) } else { Issue.record("expected board template to be a mapping, got \(String(describing: model.template))") } #expect(model.lanes.map(\.id.rawValue) == [RichBoard.laneDoing, RichBoard.laneDone]) #expect(result.warnings.isEmpty) let doing = try #require(model.lanes.first { $0.id.rawValue == RichBoard.laneDoing }) #expect(doing.title.value == "Doing") #expect(doing.width.value == 2) #expect(doing.background.value == "#3478F6") #expect(doing.cards.map(\.id.rawValue) == [RichBoard.cardTaxonomy, RichBoard.cardSecond]) let taxonomy = try #require(doing.cards.first { $0.id.rawValue == RichBoard.cardTaxonomy }) #expect(taxonomy.title.value == "Design the fixture taxonomy") #expect(taxonomy.modifiedBy.value == "claude") #expect(taxonomy.body.contains("attachments/ holds a sketch")) let done = try #require(model.lanes.first { $0.id.rawValue == RichBoard.laneDone }) #expect(done.cards.map(\.id.rawValue) == [RichBoard.cardShip]) #expect(done.cards[0].title.value == "Ship v1") } /// `attachments/` is **flat** (01-storage-format.md § Attachments): the card's listing is its /// top-level regular files and nothing else. This card's folder holds all four shapes on real /// disk — two ordinary files, a hidden one, and a subfolder with a file in it — so the rule is /// asserted against a filesystem rather than against a mock. /// /// The excluded three are excluded for three different reasons and only one of them is stated /// in the design doc: subfolders are "tolerated, preserved verbatim … and not surfaced"; the /// hidden file is the loader's uniform `.skipsHiddenFiles` stance (a `.DS_Store` is not /// anyone's attachment); symlinks are the loader's never-resolve stance, covered in /// `BoardLoaderTests` because git cannot carry that shape into a fixture reliably. @Test func aCardListsOnlyTheTopLevelFilesOfItsAttachmentsFolder() throws { let result = try loadFixture("Valid/rich-board.kanban") let doing = try #require(result.model.lanes.first { $0.id.rawValue == RichBoard.laneDoing }) let taxonomy = try #require(doing.cards.first { $0.id.rawValue == RichBoard.cardTaxonomy }) #expect(taxonomy.attachments == ["notes.txt", "sketch.png"]) } /// The overwhelmingly common shape: no `attachments/` folder at all. It reads as an empty /// listing, never as a warning or a failure — nothing has been attached yet is an ordinary /// state, and it is what makes the face's paperclip indicator absent by default. @Test func cardsWithoutAnAttachmentsFolderListNothing() throws { let result = try loadFixture("Valid/rich-board.kanban") let doing = try #require(result.model.lanes.first { $0.id.rawValue == RichBoard.laneDoing }) let second = try #require(doing.cards.first { $0.id.rawValue == RichBoard.cardSecond }) let done = try #require(result.model.lanes.first { $0.id.rawValue == RichBoard.laneDone }) #expect(second.attachments.isEmpty) #expect(done.cards.map(\.attachments) == [[]]) #expect(result.warnings.isEmpty) } @Test func boardUnknownAndReservedKeysPreserveOrder() throws { let result = try loadFixture("Valid/rich-board.kanban") // schema-owned keys (schema, title, created, modified, modified-by, background, icon, // iconColor) are filtered out; only the agent-overlay and reserved keys remain, in the // order they were written. #expect(result.model.document.unknownFields.map(\.key) == ["project", "sphere", "labels", "template"]) } /// The whole-tree round-trip guarantee (01-storage-format.md § Fractal layout: "the app /// never reformats a body it didn't change"): every `index.md` under the rich board parses /// and re-serializes to its original bytes, untouched. @Test func everyIndexMdInTheTreeRoundTripsByteIdentically() throws { let root = fixtureBoard("Valid/rich-board.kanban") let files = try allIndexMdFiles(under: root) #expect(files.count == 7) // board + 2 lanes + 3 cards + the one comment folder's index.md for file in files { let text = try String(contentsOf: file, encoding: .utf8) let document = try FrontmatterDocument.parse(text) #expect(document.serialized() == text, "\(file.path) did not round-trip byte-identically") } } } // MARK: - Valid/interrupted-create.kanban struct FixtureInterruptedCreateTests { @Test func indexlessFoldersAreSkippedNotFailed() throws { let lane = "10000000-0000-4000-8000-000000000001" let laneInterrupted = "20000000-0000-4000-8000-000000000002" let card = "30000000-0000-4000-8000-000000000003" let cardInterrupted = "40000000-0000-4000-8000-000000000004" let result = try loadFixture("Valid/interrupted-create.kanban") #expect(result.model.lanes.map(\.id.rawValue) == [lane]) #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card]) #expect(result.warnings.count == 2) #expect(result.warnings.contains(.missingIndex(path: laneInterrupted))) #expect(result.warnings.contains(.missingIndex(path: "\(lane)/\(cardInterrupted)"))) } } // MARK: - Valid/non-uuid-strays.kanban struct FixtureNonUUIDStraysTests { @Test func nonUUIDFoldersAreStraysAtEveryDepthRegardlessOfIndex() throws { let lane = "10000000-0000-4000-8000-000000000001" let card = "50000000-0000-4000-8000-000000000005" let result = try loadFixture("Valid/non-uuid-strays.kanban") #expect(result.model.lanes.map(\.id.rawValue) == [lane]) #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card]) #expect(result.warnings.count == 4) #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "todo-notes"))) #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "scratch"))) #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "\(lane)/draft"))) #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "\(lane)/wip"))) } } // MARK: - Valid/stray-files.kanban struct FixtureStrayFilesTests { @Test func strayFilesEverywhereProduceNoWarningsAndDontAffectTheModel() throws { let lane = "10000000-0000-4000-8000-000000000001" let card = "20000000-0000-4000-8000-000000000002" let result = try loadFixture("Valid/stray-files.kanban") #expect(result.warnings.isEmpty) #expect(result.model.lanes.map(\.id.rawValue) == [lane]) #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card]) } /// The board's card-level `scratch.md` is the **one** stray this fixture holds that is not /// tolerated: the loose-file carve-out (01-storage-format.md § Fractal layout ▸ Rules, settled /// 2026-07-28) says a regular file beside a card's `index.md` belongs in `attachments/`. It is /// reported on its own channel — never as a `warning`, which is the *tolerance* vocabulary — /// and the board-level and lane-level strays around it stay exactly as tolerated as they were. /// /// **Detection does not mutate**: this is the loader, over a fixture that lives in git, and the /// assertion that the file is still there afterwards is the read-only claim stated on the one /// tree where a stray write would be visible in `git status`. @Test func aCardLevelLooseFileIsReportedForRelocationWithoutBeingTouched() throws { let lane = "10000000-0000-4000-8000-000000000001" let card = "20000000-0000-4000-8000-000000000002" let result = try loadFixture("Valid/stray-files.kanban") #expect(result.warnings.isEmpty) #expect(result.looseCardFiles == [ LooseCardFiles( laneID: ItemID(rawValue: lane), cardID: ItemID(rawValue: card), title: result.model.lanes[0].cards[0].title.value, fileNames: ["scratch.md"] ), ]) let scratch = fixtureBoard("Valid/stray-files.kanban") .appendingPathComponent("\(lane)/\(card)/scratch.md") #expect(FileManager.default.fileExists(atPath: scratch.path)) #expect(!FileManager.default.fileExists( atPath: fixtureBoard("Valid/stray-files.kanban") .appendingPathComponent("\(lane)/\(card)/attachments").path )) } } // MARK: - Valid/tombstones.kanban struct FixtureTombstonesTests { @Test func tombstonedLaneAndCardStayInTheSnapshotFlagged() throws { let laneLive = "10000000-0000-4000-8000-000000000001" let laneDead = "20000000-0000-4000-8000-000000000002" let cardLive = "30000000-0000-4000-8000-000000000003" let cardDead = "40000000-0000-4000-8000-000000000004" let cardUnderDeadLane = "50000000-0000-4000-8000-000000000005" let result = try loadFixture("Valid/tombstones.kanban") // The lane's key is the tolerate tier's since 2026-07-29 — ignored, logged, and the lane // loads live (01 § Deletion, lane clause). #expect(result.warnings == [.laneLevelDeletedIgnored(path: laneDead)]) #expect(result.model.lanes.map(\.id.rawValue) == [laneLive, laneDead]) let live = try #require(result.model.lanes.first { $0.id.rawValue == laneLive }) #expect(live.isDeleted == false) #expect(live.cards.map(\.id.rawValue) == [cardLive, cardDead]) #expect(live.cards.map(\.isDeleted) == [false, true]) let dead = try #require(result.model.lanes.first { $0.id.rawValue == laneDead }) #expect(dead.isDeleted == true) #expect(dead.cards.map(\.id.rawValue) == [cardUnderDeadLane]) // A tombstoned lane doesn't propagate deletion onto its children's own flag — the // loader is structural, not recursive; hiding an ancestor's tombstoned subtree is a // rendering concern, not a load-time one. #expect(dead.cards[0].isDeleted == false) } } // MARK: - Valid/duplicate-order-tie-break.kanban struct FixtureDuplicateOrderTieBreakTests { @Test func tiedLanesAndTiedCardsBreakByFolderNameAscending() throws { let laneA = "10000000-0000-4000-8000-000000000001" let laneAAA = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" let laneBBB = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" // Distinct from `laneA` on purpose: a lane and one of its own cards sharing a UUID is a // duplicate id board-wide, which the loader's dedupe withholds (01-storage-format.md // § Fractal layout ▸ Rules). It sorts before `cardY` either way, which is all this fixture // ever needed of it. let cardX = "10000000-0000-4000-8000-00000000000a" let cardY = "20000000-0000-4000-8000-000000000002" let cardZ = "30000000-0000-4000-8000-000000000003" let result = try loadFixture("Valid/duplicate-order-tie-break.kanban") // laneA (order 1024) first; laneAAA and laneBBB tie at 2048, broken 'a' < 'b'. #expect(result.model.lanes.map(\.id.rawValue) == [laneA, laneAAA, laneBBB]) let lane = try #require(result.model.lanes.first { $0.id.rawValue == laneA }) #expect(lane.cards.map(\.id.rawValue) == [cardX, cardY, cardZ]) #expect(Set(lane.cards.map(\.order)) == [1024]) } } // MARK: - Valid/unknown-key-order.kanban struct FixtureUnknownKeyOrderTests { @Test func unknownKeysPreserveDocumentOrderAtEveryLevel() throws { let result = try loadFixture("Valid/unknown-key-order.kanban") let model = result.model #expect(model.document.unknownFields.map(\.key) == ["project", "sphere", "template", "labels", "custom-note"]) let lane = try #require(model.lanes.first) #expect(lane.document.unknownFields.map(\.key) == ["remote-state", "assignees", "due", "custom"]) let card = try #require(lane.cards.first) #expect(card.document.unknownFields.map(\.key) == ["labels", "assignees", "due", "remote", "agent-scratch"]) } @Test func everyIndexMdRoundTripsByteIdentically() throws { let root = fixtureBoard("Valid/unknown-key-order.kanban") for file in try allIndexMdFiles(under: root) { let text = try String(contentsOf: file, encoding: .utf8) #expect(try FrontmatterDocument.parse(text).serialized() == text) } } } // MARK: - Valid/coercion.kanban struct FixtureCoercionTests { @Test func wrongTypeScalarsCoerceOrFallBackToDefaultPerField() throws { let lane1 = "10000000-0000-4000-8000-000000000001" let lane2 = "20000000-0000-4000-8000-000000000002" let cardTitleInt = "30000000-0000-4000-8000-000000000003" let cardTitleSeq = "40000000-0000-4000-8000-000000000004" let cardIconColorInt = "50000000-0000-4000-8000-000000000005" let cardBackgroundMap = "60000000-0000-4000-8000-000000000006" let cardBackgroundScalar = "70000000-0000-4000-8000-000000000007" let cardDeletedBad = "80000000-0000-4000-8000-000000000008" let result = try loadFixture("Valid/coercion.kanban") let model = result.model let laneWidthCoerces = try #require(model.lanes.first { $0.id.rawValue == lane1 }) #expect(laneWidthCoerces.width == .valid(3)) // width: "3" (quoted string) coerces let laneWidthMalformed = try #require(model.lanes.first { $0.id.rawValue == lane2 }) #expect(laneWidthMalformed.width == .malformed(raw: "1.5")) // non-integer, no sensible width func card(_ id: String) throws -> Card { try #require(laneWidthCoerces.cards.first { $0.id.rawValue == id }) } #expect(try card(cardTitleInt).title == .valid("2048")) #expect(try card(cardTitleSeq).title == .malformed(raw: "[a, b]")) #expect(try card(cardIconColorInt).iconColor == .valid("42")) // **`background` is a mapping and only a mapping** (01-storage-format.md § Frontmatter, // ruled 2026-08-06). So the two background cards say opposite things about one key: // `{x: 1}` is a perfectly legal mapping that simply names neither subkey — no colour, no // trace, the unknown subkey riding along like any unknown key — while the bare scalar // `12345` has no reading at all, which is the golden pin on the retired shape. #expect(try card(cardBackgroundMap).background == .missing) #expect(try card(cardBackgroundScalar).background == .malformed(raw: "12345")) #expect(try card(cardBackgroundScalar).background.rawText == "12345") let deletedBad = try card(cardDeletedBad) #expect(deletedBad.deleted == .malformed(raw: "definitely-not-a-date")) // Presence outranks validity: an unusable timestamp still tombstones. #expect(deletedBad.isDeleted == true) } } // MARK: - Valid/duplicate-top-level-keys.kanban struct FixtureDuplicateTopLevelKeysTests { @Test func lastOccurrenceWinsAtBoardLaneAndCardLevel() throws { let lane = "10000000-0000-4000-8000-000000000001" let card = "20000000-0000-4000-8000-000000000002" let result = try loadFixture("Valid/duplicate-top-level-keys.kanban") #expect(result.warnings.isEmpty) // NOT a fail-fast case, per the newer design-doc rule #expect(result.model.title == .valid("Final Name")) let loadedLane = try #require(result.model.lanes.first { $0.id.rawValue == lane }) #expect(loadedLane.order == 4096) // duplicated 'order' — a strict field — still last-wins let loadedCard = try #require(loadedLane.cards.first { $0.id.rawValue == card }) #expect(loadedCard.title == .valid("Second Title")) } /// Earlier occurrences of a duplicated key are invisible to every read but still preserved /// verbatim on disk (01-storage-format.md § Frontmatter) — proven by round-tripping every /// file in this board, not just asserting the winning value. @Test func earlierOccurrencesSurviveOnDiskViaRoundTrip() throws { let root = fixtureBoard("Valid/duplicate-top-level-keys.kanban") let files = try allIndexMdFiles(under: root) #expect(files.count == 3) for file in files { let text = try String(contentsOf: file, encoding: .utf8) #expect(try FrontmatterDocument.parse(text).serialized() == text) } } } // MARK: - Valid/board-level-deleted.kanban struct FixtureBoardLevelDeletedTests { @Test func boardLevelDeletedIsIgnoredButRestOfBoardLoadsNormally() throws { let lane = "10000000-0000-4000-8000-000000000001" let card = "20000000-0000-4000-8000-000000000002" let result = try loadFixture("Valid/board-level-deleted.kanban") #expect(result.warnings.contains(.boardLevelDeletedIgnored)) #expect(result.model.deleted == .valid(iso8601("2026-01-01T00:00:00Z"))) // Meaningless at board level, but never blanks the board — the rest loads as usual. #expect(result.model.lanes.map(\.id.rawValue) == [lane]) #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card]) } } // MARK: - Valid/optional-keys.kanban /// **The optional-key ruling's golden board** (01-storage-format.md § Frontmatter and § Ordering, /// re-ruled 2026-07-31): below the board root `order` and `schema` are optional, a missing or /// unusable `order` reads as append-at-end, and a missing `schema` reads as 1. Every shape that used /// to have its own board under `Malformed/` lives here instead, as a coercion case. private enum OptionalKeys { static let rankedLane = "10000000-0000-4000-8000-000000000001" static let schemalessLane = "40000000-0000-4000-8000-000000000002" static let orderlessLane = "30000000-0000-4000-8000-000000000003" static let rankedCard = "20000000-0000-4000-8000-000000000001" static let minimumCard = "20000000-0000-4000-8000-000000000002" static let nullOrderCard = "20000000-0000-4000-8000-000000000003" static let nonNumericCard = "20000000-0000-4000-8000-000000000004" static let nonFiniteCard = "20000000-0000-4000-8000-000000000005" } struct FixtureOptionalKeysTests { /// The strip: ranked lanes first in ascending order, then the order-less one — and the /// `schema`-less lane is an ordinary ranked lane, since only its `schema` was absent. @Test func orderlessLaneSortsAfterEveryRankedOne() throws { let model = try loadFixture("Valid/optional-keys.kanban").model #expect(model.lanes.map(\.id.rawValue) == [ OptionalKeys.rankedLane, OptionalKeys.schemalessLane, OptionalKeys.orderlessLane, ]) #expect(model.lanes.map(\.order) == [1024, 2048, 3072]) } /// Four order-less cards behind one ranked one, in folder-name order — the tie-break the ruling /// states the reading in, and the accepted cost it names ("two order-less siblings sort by UUID /// rather than by intent until touched"). @Test func orderlessCardsAppendInFolderNameOrder() throws { let model = try loadFixture("Valid/optional-keys.kanban").model let lane = try #require(model.lanes.first { $0.id.rawValue == OptionalKeys.rankedLane }) #expect(lane.cards.map(\.id.rawValue) == [ OptionalKeys.rankedCard, OptionalKeys.minimumCard, OptionalKeys.nullOrderCard, OptionalKeys.nonNumericCard, OptionalKeys.nonFiniteCard, ]) // `append`'s own arithmetic, which is what makes the reading stampable verbatim. #expect(lane.cards.map(\.order) == [1024, 2048, 3072, 4096, 5120]) } /// A missing `schema` below the root reads as 1 — at both levels. @Test func missingSchemaBelowTheRootReadsAsOne() throws { let model = try loadFixture("Valid/optional-keys.kanban").model let lane = try #require(model.lanes.first { $0.id.rawValue == OptionalKeys.schemalessLane }) #expect(lane.schema == 1) let ranked = try #require(model.lanes.first { $0.id.rawValue == OptionalKeys.rankedLane }) let minimum = try #require(ranked.cards.first { $0.id.rawValue == OptionalKeys.minimumCard }) #expect(minimum.schema == 1) #expect(minimum.title == .valid("Minimum Agent Card")) } /// Every reading leaves a coerce-tier trace: field, path, and the text as written — an absent /// key having none to record (01-storage-format.md § Frontmatter, the family posture). @Test func everyReadingIsRecordedAsACoercion() throws { let result = try loadFixture("Valid/optional-keys.kanban") let byPath = Dictionary( uniqueKeysWithValues: result.coercedFrontmatter.map { ($0.path, $0.fields) }) func fields(_ path: String) throws -> [CoercedField] { try #require(byPath[path], "no coercion recorded for \(path)") } #expect(try fields("\(OptionalKeys.orderlessLane)/index.md") == [CoercedField(key: "order", raw: "")]) #expect(try fields("\(OptionalKeys.schemalessLane)/index.md") == [CoercedField(key: "schema", raw: "")]) let lane = OptionalKeys.rankedLane #expect(try fields("\(lane)/\(OptionalKeys.minimumCard)/index.md") == [ CoercedField(key: "schema", raw: ""), CoercedField(key: "order", raw: ""), ]) #expect(try fields("\(lane)/\(OptionalKeys.nullOrderCard)/index.md") == [CoercedField(key: "order", raw: "")]) #expect(try fields("\(lane)/\(OptionalKeys.nonNumericCard)/index.md") == [CoercedField(key: "order", raw: "banana")]) #expect(try fields("\(lane)/\(OptionalKeys.nonFiniteCard)/index.md") == [CoercedField(key: "order", raw: ".nan")]) // Coerce-tier means read-side only: nothing here is work, so nothing carries a heal class. #expect(result.defects.allSatisfy { $0.healClass == nil }) } /// The whole board loads clean — no warnings, no fail-fast, and every file byte-identical after /// a parse/serialize round-trip: the bytes are preserved verbatim, which is the coerce tier's /// other half. @Test func loadsWithoutWarningsAndRoundTrips() throws { let result = try loadFixture("Valid/optional-keys.kanban") #expect(result.warnings.isEmpty) for file in try allIndexMdFiles(under: fixtureBoard("Valid/optional-keys.kanban")) { let text = try String(contentsOf: file, encoding: .utf8) #expect(try FrontmatterDocument.parse(text).serialized() == text) } } } // MARK: - Malformed/*.kanban — fail-fast cases struct FixtureMalformedTests { @Test func unparseableYAML() { expectFixtureFailure("Malformed/unparseable-yaml.kanban", path: "index.md", reasonDescription: "unparseableYAML") { if case .unparseableYAML = $0 { true } else { false } } } /// **The board root's own `schema` is still required** (re-ruled 2026-07-31 — the /// this-really-is-a-board gate). Its below-the-root twin is `FixtureOptionalKeysTests`, where /// the same absence reads as 1. @Test func missingSchema() { expectFixtureFailure("Malformed/missing-schema.kanban", path: "index.md", reasonDescription: "missingSchema") { $0 == .missingSchema } } @Test func schemaNewerThanApp() { expectFixtureFailure( "Malformed/schema-newer-than-app.kanban", path: "index.md", reasonDescription: "schemaNewerThanApp(2)" ) { $0 == .schemaNewerThanApp(found: 2) } } @Test func boardRootMissingIndex() { expectFixtureFailure( "Malformed/board-root-missing-index.kanban", path: "index.md", reasonDescription: "boardRootMissingIndex" ) { $0 == .boardRootMissingIndex } } } // MARK: - Malformed/many-defects.kanban — the collect-all board /// **The loader collects every fail-fast defect in the walk rather than stopping at the first** /// (01-storage-format.md § Malformed input, settled 2026-07-31 — the decision surface's whole /// premise: "one aggregated surface presents them all", never a chain of modals). /// /// Three classes on one board, on real disk: the root's own missing `schema`, a card written by a /// newer Lanework, and a lane whose frontmatter will not parse. private enum ManyDefects { static let board = "Malformed/many-defects.kanban" static let intactLane = "10000000-0000-4000-8000-000000000001" static let brokenLane = "30000000-0000-4000-8000-000000000002" static let tailLane = "50000000-0000-4000-8000-000000000003" static let intactCard = "20000000-0000-4000-8000-000000000001" static let newerCard = "20000000-0000-4000-8000-000000000002" /// Broken, and behind the broken lane — never enumerated, so never in the aggregate. static let cardBehindTheBrokenLane = "40000000-0000-4000-8000-000000000009" static let tailCard = "60000000-0000-4000-8000-000000000004" } struct FixtureManyDefectsTests { /// The whole list, in walk order, asserted as a list — the ordering *is* the contract, because it /// is what the surface groups top-down and what `primary` reads off. @Test func everyFailFastDefectIsCollectedInWalkOrder() { do { _ = try loadFixture(ManyDefects.board) Issue.record("a board with three fail-fast defects loaded") } catch let failure as BoardLoadFailure { // The paths, as a list, because the *order* is the contract: the root first — its own // `schema` is the this-really-is-a-board gate, and the walk continued past it because // nothing below reads the root's document — then lanes in folder-name order with each // lane's cards inside it. #expect(failure.defects.map(\.path) == [ "index.md", "\(ManyDefects.intactLane)/\(ManyDefects.newerCard)/index.md", "\(ManyDefects.brokenLane)/index.md", ]) guard failure.defects.count == 3 else { return } #expect(failure.defects[0].reason == .missingSchema) #expect(failure.defects[1].reason == .schemaNewerThanApp(found: 2)) // The lane's reason is matched by shape rather than by the parser's exact sentence, // which is the YAML engine's wording and not this suite's to pin. if case .unparseableYAML = failure.defects[2].reason {} else { Issue.record("expected unparseable YAML on the lane, got \(failure.defects[2].reason)") } } catch { Issue.record("expected a BoardLoadFailure, got \(error)") } } /// **A broken lane takes its subtree with it, and that is the designed loop.** The card under the /// unparseable lane is broken too and is deliberately absent from the aggregate: it was never /// enumerated. Repair the lane, Re-check — "re-runs the whole walk" — and the next aggregate /// carries it. Deeper defects surface one repair at a time, by design, not by omission. @Test func aBrokenLanesSubtreeIsNeverEnumerated() { do { _ = try loadFixture(ManyDefects.board) Issue.record("a board with three fail-fast defects loaded") } catch let failure as BoardLoadFailure { #expect(!failure.defects.contains { $0.path.contains(ManyDefects.cardBehindTheBrokenLane) }) } catch { Issue.record("expected a BoardLoadFailure, got \(error)") } } /// `primary` is the walk's first defect — the outermost problem, which is the one a one-line /// surface should name. @Test func primaryIsTheRootsOwnDefect() { do { _ = try loadFixture(ManyDefects.board) Issue.record("a board with three fail-fast defects loaded") } catch let failure as BoardLoadFailure { #expect(failure.primary == BoardLoadError(path: "index.md", reason: .missingSchema)) #expect(failure.description.hasSuffix("(and 2 more)")) } catch { Issue.record("expected a BoardLoadFailure, got \(error)") } } /// **The root is unskippable** (01-storage-format.md § Malformed input): the surface never offers /// Skip there — the root defects have minted repairs, and a newer root schema is Cancel-only — so /// a set naming the root's `index.md` is ignored rather than obeyed. Policed in the loader, which /// is what makes a board with no root document impossible to construct. @Test func aSkipSetNamingTheRootIsIgnored() { do { _ = try BoardLoader.load( boardRoot: fixtureBoard(ManyDefects.board), skipping: [ "index.md", ".", "\(ManyDefects.intactLane)/\(ManyDefects.newerCard)/index.md", "\(ManyDefects.brokenLane)/index.md", ] ) Issue.record("the root's own defect was skipped — a board with no schema loaded") } catch let failure as BoardLoadFailure { // Exactly the root's, and nothing else: the two below-root entries were honoured. #expect(failure.defects == [BoardLoadError(path: "index.md", reason: .missingSchema)]) } catch { Issue.record("expected a BoardLoadFailure, got \(error)") } } } // MARK: - Malformed/skippable-defects.kanban — the skip channel /// **"Skip is user-consented tolerance, loudly marked"** (01-storage-format.md § Malformed input, /// ruled 2026-07-31): a skipped item loads the board without it, the file stays on disk untouched, /// and the opened board carries a warning naming what left. Per-open, never persisted — which this /// suite gets for free, because the set is an argument. private enum SkippableDefects { static let board = "Malformed/skippable-defects.kanban" static let intactLane = "10000000-0000-4000-8000-000000000001" static let brokenLane = "30000000-0000-4000-8000-000000000002" static let tailLane = "50000000-0000-4000-8000-000000000003" static let intactCard = "20000000-0000-4000-8000-000000000001" static let newerCard = "20000000-0000-4000-8000-000000000002" static let cardBehindTheBrokenLane = "40000000-0000-4000-8000-000000000009" static let tailCard = "60000000-0000-4000-8000-000000000004" static let newerCardPath = "\(intactLane)/\(newerCard)/index.md" static let brokenLanePath = "\(brokenLane)/index.md" } struct FixtureSkippableDefectsTests { /// Unskipped, the board is an ordinary two-defect refusal — the baseline the skips are measured /// against, and the proof the fixture is broken in exactly two places. @Test func withoutSkipsTheBoardRefusesWithBothDefects() { do { _ = try loadFixture(SkippableDefects.board) Issue.record("a board with two fail-fast defects loaded") } catch let failure as BoardLoadFailure { #expect(failure.defects.map(\.path) == [ SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath, ]) } catch { Issue.record("expected a BoardLoadFailure, got \(error)") } } /// Skip both and the board opens — **and each skipped item leaves with its whole subtree**. The /// broken lane's own card is perfectly valid and is gone too: that is the honest cost of the /// tolerance, and the reason the notice names what left rather than pretending nothing did. @Test func skippingEveryDefectLoadsTheBoardWithoutThoseItems() throws { let result = try BoardLoader.load( boardRoot: fixtureBoard(SkippableDefects.board), skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath] ) #expect(result.model.title.value == "Skippable Defects") #expect(result.model.lanes.map(\.id.rawValue) == [ SkippableDefects.intactLane, SkippableDefects.tailLane, ]) let intact = try #require(result.model.lanes.first { $0.id.rawValue == SkippableDefects.intactLane }) #expect(intact.cards.map(\.id.rawValue) == [SkippableDefects.intactCard]) // The subtree went with the lane, valid card and all. let everyCard = result.model.lanes.flatMap { $0.cards.map(\.id.rawValue) } #expect(!everyCard.contains(SkippableDefects.cardBehindTheBrokenLane)) #expect(everyCard == [SkippableDefects.intactCard, SkippableDefects.tailCard]) } /// The loud mark: one warning per skip, naming the defect's own path — which is what the surface /// row named and what the opened board's notice resolves its Reveal in Finder against. @Test func everySkipIsWarnedAbout() throws { let result = try BoardLoader.load( boardRoot: fixtureBoard(SkippableDefects.board), skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath] ) #expect(result.warnings == [ .userSkipped(path: SkippableDefects.newerCardPath), .userSkipped(path: SkippableDefects.brokenLanePath), ]) } /// A partial skip is an ordinary refusal over what is left — the surface's per-item override, /// and the reason Skip is a set rather than a switch. @Test func skippingOneDefectStillRefusesForTheOther() { do { _ = try BoardLoader.load( boardRoot: fixtureBoard(SkippableDefects.board), skipping: [SkippableDefects.newerCardPath] ) Issue.record("the unskipped lane defect did not refuse the load") } catch let failure as BoardLoadFailure { #expect(failure.defects.map(\.path) == [SkippableDefects.brokenLanePath]) } catch { Issue.record("expected a BoardLoadFailure, got \(error)") } } /// **Skip touches nothing on disk** — "the file stays on disk untouched, tolerated-invisible like /// strays". Asserted on the one tree where a stray write would show up in `git status`. @Test func aSkippedItemIsLeftExactlyWhereItIs() throws { let root = fixtureBoard(SkippableDefects.board) let before = try allIndexMdFiles(under: root).map(\.path).sorted() _ = try BoardLoader.load( boardRoot: root, skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath] ) #expect(try allIndexMdFiles(under: root).map(\.path).sorted() == before) let skipped = root.appendingPathComponent(SkippableDefects.brokenLanePath) let text = try String(contentsOf: skipped, encoding: .utf8) #expect(text.contains("labels: [red, green"), "the skipped file was rewritten") } } // MARK: - Memo-vs-cold equivalence, over every golden board /// **Result-purity with cost unspecified** (02-architecture.md § Live-reload resilience, blessed /// 2026-07-31: "The loader's contract is result-purity with cost unspecified: same tree in, same /// snapshot out, and the memo can only change how fast"). /// /// The memo is the one thing in the loader that could make two walks of the same tree disagree, so /// the claim is stated where the trees are real and hand-authored: every golden board is walked cold, /// then walked again with the first walk's memo, and the two results are compared whole. Nothing here /// edits a tree — the mechanism's per-file behaviour is `BoardLoaderParseMemoTests`' subject; this is /// the equivalence, across every shape the fixture corpus holds. /// /// The malformed boards are included on purpose. A refusal is a result too, and a memo that changed /// which defects a walk collected — or their order — would be the worst possible way for this to be /// wrong, since the decision surface is written directly against that list. struct FixtureMemoEquivalenceTests { /// Every board under `Fixtures/Valid`, by relative path. static let validBoards = [ "Valid/rich-board.kanban", "Valid/interrupted-create.kanban", "Valid/non-uuid-strays.kanban", "Valid/stray-files.kanban", "Valid/tombstones.kanban", "Valid/duplicate-order-tie-break.kanban", "Valid/unknown-key-order.kanban", "Valid/coercion.kanban", "Valid/duplicate-top-level-keys.kanban", "Valid/board-level-deleted.kanban", "Valid/optional-keys.kanban", ] /// Every board under `Fixtures/Malformed`, by relative path. static let malformedBoards = [ "Malformed/board-root-missing-index.kanban", "Malformed/missing-schema.kanban", "Malformed/unparseable-yaml.kanban", "Malformed/schema-newer-than-app.kanban", "Malformed/many-defects.kanban", "Malformed/skippable-defects.kanban", ] @Test("A memoized walk of a valid board is the cold walk, whole", arguments: validBoards) func aMemoizedWalkMatchesTheColdWalk(board: String) throws { let cold = try loadFixture(board) let warm = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: cold.memo) #expect(warm.model == cold.model) #expect(warm.warnings == cold.warnings) #expect(warm.defects == cold.defects) #expect(warm.trashKinds == cold.trashKinds) // The memo the second walk produced can stand in for the first's, which is what makes the // store's chain of reloads self-sustaining rather than degrading walk by walk. #expect(warm.memo.count == cold.memo.count) } @Test("A memoized walk of a valid board opens no index.md at all", arguments: validBoards) func aMemoizedWalkReadsNothing(board: String) throws { let cold = try loadFixture(board) let counter = BoardLoader.ParseCounter() _ = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: cold.memo, counter: counter) #expect(counter.counts.parsed == 0) #expect(counter.counts.reused == cold.memo.count) #expect(counter.counts.reused > 0, "a fixture with nothing to memoize proves nothing") } @Test("A memoized walk of a malformed board refuses identically", arguments: malformedBoards) func aMemoizedWalkRefusesIdentically(board: String) { // A refusing walk hands back no memo — that is the design, not an omission: a defective file // is never recorded, and a walk that threw produced no `LoadResult` to carry one on. So the // memo under test is the empty one a first walk would offer, and what is pinned is that the // memo parameter never moves the aggregate a refusal reports. let defects = refusal(board, memo: nil) #expect(defects == refusal(board, memo: BoardLoader.ParseMemo())) #expect(!defects.isEmpty, "\(board) is in the malformed corpus but loaded") } /// One malformed board's aggregate, or `[]` where it unexpectedly loaded. private func refusal(_ board: String, memo: BoardLoader.ParseMemo?) -> [BoardLoadError] { do throws(BoardLoadFailure) { _ = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: memo) return [] } catch { return error.defects } } /// The malformed corpus' real memo case: a board that refuses is repaired-by-skip into one that /// loads, and the memo that produced carries into the next walk without moving the answer. @Test("A skipped-open board reloads through its own memo unchanged") func aSkippedOpenReloadsUnchanged() throws { let skips: Set = [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath] let root = fixtureBoard(SkippableDefects.board) let cold = try BoardLoader.load(boardRoot: root, skipping: skips) let warm = try BoardLoader.load(boardRoot: root, skipping: skips, memo: cold.memo) #expect(warm.model == cold.model) #expect(warm.warnings == cold.warnings) #expect(warm.defects == cold.defects) } }