Write the write-path fidelity test suite

The executable spec for BoardWriter's cross-cutting guarantees, in a
dedicated WriteFidelityTests.swift: minimal-touch verified down to
sibling mtimes across a seven-operation session; renumber pinned as
the sole exception (whole lane rewritten, board root / other lanes /
tombstones untouched); delete→restore byte-precise (only the modified
line differs, zero tombstone residue); unknown-key order preserved
with keys interleaved among schema-owned ones; and a composite
building a board purely through the writer, hand-editing a file like
an agent would, then moving/copying/deleting/renumbering to a
zero-warning reload. Guarantees already pinned by the per-operation
suites (atomicity, move/copy identity, body round-trip) are
referenced, not duplicated. Shared fixtures promoted to
WriterTestSupport.swift. README gains the write-side feature bullet.

5 new tests; 250 total green. Closes milestone m2-storage-write.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 18:04:57 -04:00
parent 840528d14c
commit 328cfb629f
4 changed files with 584 additions and 138 deletions
+3 -138
View File
@@ -4,90 +4,7 @@ import Testing
// MARK: - Fixtures
/// A temp directory holding hand-written `index.md` files, written and read back as raw bytes
/// so every assertion here is about what is actually on disk the writer's whole contract
/// (02-architecture.md § Layering Components, "a write is done when the file is on disk").
private struct WriterFixture {
let root: URL
init() throws {
root = FileManager.default.temporaryDirectory
.appendingPathComponent("BoardWriterTests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
}
/// Restores permissions before removing: the atomicity test deliberately makes a folder
/// unwritable, and an unwritable folder is also an unremovable one.
func tearDown() {
let manager = FileManager.default
if let walker = manager.enumerator(atPath: root.path) {
for case let relative as String in walker {
try? manager.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: root.appendingPathComponent(relative).path
)
}
}
try? manager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: root.path)
try? manager.removeItem(at: root)
}
func url(_ relativePath: String) -> URL {
relativePath.isEmpty ? root : root.appendingPathComponent(relativePath, isDirectory: true)
}
/// Writes `text` verbatim (BOM-less UTF-8, line endings exactly as given) to
/// `<relativePath>/index.md`.
@discardableResult
func item(_ relativePath: String, _ text: String) throws -> URL {
try write(Data(text.utf8), to: relativePath)
}
@discardableResult
func item(_ relativePath: String, bytes: Data) throws -> URL {
try write(bytes, to: relativePath)
}
@discardableResult
private func write(_ data: Data, to relativePath: String) throws -> URL {
let folder = url(relativePath)
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
try data.write(to: folder.appendingPathComponent("index.md"))
return folder
}
/// Writes an arbitrary file not an `index.md` creating its folder: attachments and
/// strays, the content a copy has to carry verbatim without ever reading it.
@discardableResult
func file(_ relativePath: String, _ bytes: Data) throws -> URL {
let fileURL = root.appendingPathComponent(relativePath)
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try bytes.write(to: fileURL)
return fileURL
}
func data(_ relativePath: String) throws -> Data {
try Data(contentsOf: root.appendingPathComponent(relativePath))
}
func exists(_ relativePath: String) -> Bool {
FileManager.default.fileExists(atPath: url(relativePath).path)
}
func indexData(_ relativePath: String) throws -> Data {
try Data(contentsOf: url(relativePath).appendingPathComponent("index.md"))
}
func indexText(_ relativePath: String) throws -> String {
try String(decoding: indexData(relativePath), as: UTF8.self)
}
/// Every entry in the folder, hidden ones included the writer's temp files are hidden, so
/// only a listing that sees them can prove there is no residue.
func entryNames(_ relativePath: String) throws -> [String] {
try FileManager.default.contentsOfDirectory(atPath: url(relativePath).path).sorted()
}
}
// `WriterFixture` lives in `WriterTestSupport.swift` shared with `WriteFidelityTests.swift`.
private enum Fixture {
/// Unknown keys in a deliberate order, an own-line comment above and below, an inline
@@ -140,48 +57,7 @@ private enum Child {
static let indexless = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"
}
/// Literal UUID-shaped names for the move/copy suites, which need more of them than `Child`
/// offers an import-boundary test has the *same* identity living in two boards at once, and a
/// compound arrival needs a lane with several cards.
private enum Ident {
static let lane1 = "11111111-1111-4111-8111-111111111111"
static let lane2 = "22222222-2222-4222-8222-222222222222"
static let lane3 = "33333333-3333-4333-8333-333333333333"
static let lane4 = "44444444-4444-4444-8444-444444444444"
static let card1 = "55555555-5555-4555-8555-555555555555"
static let card2 = "66666666-6666-4666-8666-666666666666"
static let card3 = "77777777-7777-4777-8777-777777777777"
static let card4 = "99999999-9999-4999-8999-999999999999"
static let indexless = "88888888-8888-4888-8888-888888888888"
}
/// The `index.md` texts the move/copy suites move and copy around.
private enum Item {
static let board = "---\nschema: 1\ntitle: Board\n---\nBoard description.\n"
/// Everything a move or a copy has to leave alone: unknown keys with an inline comment, a
/// `created` stamp from before today, a foreign `modified-by`, and a body.
static func rich(order: String, title: String) -> String {
"""
---
schema: 1
title: \(title)
order: \(order)
project: lanework # agent overlay
labels: [a, b, c]
created: 2026-01-01T09:00:00Z
modified: 2026-02-02T09:00:00Z
modified-by: claude
---
\(title) body — with *markdown*.
"""
}
/// A whole-frontmatter flow mapping carrying a `modified-by`: readable, uneditable, and so
/// left byte-verbatim by a copy stale attribution included.
static let uneditable = "---\n{schema: 1, order: 1024, title: Odd, modified-by: claude}\n---\nodd body\n"
}
// `Ident` and `Item` live in `WriterTestSupport.swift` shared with `WriteFidelityTests.swift`.
/// The keys a move or a copy is allowed to have touched; every other line must be byte-identical.
private let rewrittenKeys = [FrontmatterKeys.order, FrontmatterKeys.modified, FrontmatterKeys.modifiedBy]
@@ -201,18 +77,7 @@ private func childrenByTitle(of relativePath: String, in fixture: WriterFixture)
return byTitle
}
private func writeFailure(_ operation: () throws -> Void) -> BoardWriteError? {
do {
try operation()
Issue.record("expected the write to fail, but it succeeded")
return nil
} catch let error as BoardWriteError {
return error
} catch {
Issue.record("expected a BoardWriteError, got \(error)")
return nil
}
}
// `writeFailure` lives in `WriterTestSupport.swift` shared with `WriteFidelityTests.swift`.
/// The file's lines minus every line that opens one of `keys` what a rewrite of exactly those
/// keys has to leave byte-identical.
+423
View File
@@ -0,0 +1,423 @@
import Foundation
import Testing
@testable import Kanban
/// The executable spec for the write path's fidelity guarantees the m2 "write-path fidelity
/// test suite" card. `BoardWriterTests.swift` already pins per-operation behavior file by file
/// (one suite per `BoardWriter` call); this file pins the guarantees that cut *across* calls and
/// across the whole tree, the properties no single per-operation test is positioned to state.
///
/// Four of the card's six guarantees are already fully pinned in `BoardWriterTests.swift` and are
/// not repeated here only referenced:
///
/// - **Byte-identical body round-trip**, by direct byte comparison
/// `BoardWriterPreservationTests.aFrontmatterEditLeavesTheBodyByteIdentical`.
/// - **Atomicity**: temp-file+rename leaves no residue on success
/// `BoardWriterPreservationTests.aSuccessfulWriteLeavesNoTempFileBehind`; an injected mid-write
/// failure (an unwritable parent folder) leaves the original bytes *and* the directory listing
/// untouched, temp file included `BoardWriterFailureTests
/// .aFailedWriteLeavesTheFileAndTheFolderExactlyAsTheyWere`. Audited: both halves of "verifiable
/// temp-file+rename, nothing left behind either way" already exist; there is no missing piece.
/// - **Move/copy identity** UUID preservation, fresh minting, and the import-boundary collision
/// repair, including the per-folder compound case (a lane arriving with one colliding card stays
/// a lane *move*) `BoardWriterMoveTests`/`BoardWriterCopyTests` in full, in particular
/// `aLaneMoveRemintsOnlyTheCollidingCard`.
///
/// What follows fills the remaining gaps: minimal-touch stated with **mtimes**, not just bytes
/// (existing sibling-byte assertions never look at the filesystem's own "did this file move"
/// signal); the renumber fallback stated as the *positive* exception across a whole board rather
/// than within one lane; deleterestore at full byte precision against the pre-delete original
/// (existing coverage checks the *result* is undeleted and reordered correctly, not that the
/// bytes differ from the original by exactly one line); unknown-key order through a writer op
/// with keys deliberately interleaved among schema-owned ones (existing coverage groups the
/// unknown keys together); and one end-to-end composite scenario tying every guarantee together.
// MARK: - Shared helpers
/// A file's bytes and on-disk modification date the two facts "minimal touch" promises stay
/// put for every `index.md` a mutation does not target.
private func snapshot(_ fixture: WriterFixture, _ relativePath: String) throws -> (data: Data, modified: Date) {
let indexURL = fixture.url(relativePath).appendingPathComponent("index.md")
let data = try Data(contentsOf: indexURL)
let attributes = try FileManager.default.attributesOfItem(atPath: indexURL.path)
guard let modified = attributes[.modificationDate] as? Date else {
Issue.record("no modification date for \(relativePath)")
return (data, .distantPast)
}
return (data, modified)
}
/// Every folder under the fixture root holding an `index.md`, relative to the root (`""` for the
/// root itself) recomputed on demand since `createCard`/`createLane` grow the set mid-test.
private func allIndexFolders(_ fixture: WriterFixture) -> [String] {
guard let walker = FileManager.default.enumerator(
at: fixture.root, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]
) else { return [] }
let rootPath = fixture.root.standardizedFileURL.path
var folders: [String] = []
for case let url as URL in walker where url.lastPathComponent == "index.md" {
let folderPath = url.deletingLastPathComponent().standardizedFileURL.path
if folderPath == rootPath {
folders.append("")
} else if folderPath.hasPrefix(rootPath + "/") {
folders.append(String(folderPath.dropFirst(rootPath.count + 1)))
}
}
return folders.sorted()
}
/// The file's lines minus every line that opens one of `keys` a local twin of
/// `BoardWriterTests.swift`'s file-private `lines(of:excludingKeys:)`; each file keeps its own
/// since the declaration is `private` (file-scoped) and this file's fixtures are its own.
private func lines(of text: String, excludingKeys keys: [String]) -> [String] {
text.components(separatedBy: "\n")
.filter { line in !keys.contains { line.hasPrefix("\($0):") } }
}
/// A couple of extra literal identities beyond `Ident`'s: this file's boards run wider (2×2
/// grids, a renumber lane with an "other lane" alongside it) than `Ident`'s four lane/card slots
/// cover on their own.
private enum MoreIdent {
static let card5 = "aaaaaaaa-1111-4111-8111-111111111112"
}
// MARK: - Minimal touch, with mtimes
/// 01-storage-format.md § Ordering, "a reorder rewrites only the moved item's `index.md`", and §
/// Fractal layout Rules' round-trip guarantee the mtime half. A same-content rewrite is
/// invisible to a byte comparison but not to the filesystem, so "untouched" has to mean both.
struct WriteFidelityMinimalTouchTests {
/// One realistic editing session reorder, style, rename, delete, restore, create, import
/// run back to back on a 2-lane × 2-card board. After each step, every `index.md` the step
/// did not target is asserted byte-identical *and* mtime-identical to its state just before
/// that step; the targeted file is asserted to have actually changed (proof the snapshot
/// method is sensitive to a real edit, not vacuously passing). The last step an attachment
/// import targets *nothing*: `importAttachments` never opens an `index.md` at all, so even
/// the host card's own frontmatter must come through unmoved.
@Test func aSequenceOfEverydayMutationsLeavesEveryOtherIndexByteAndMtimeIdentical() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Card One"))
try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Card Two"))
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Card Three"))
try fixture.item("\(Ident.lane2)/\(Ident.card4)", Item.rich(order: "2048", title: "Card Four"))
var snapshots: [String: (data: Data, modified: Date)] = [:]
for folder in allIndexFolders(fixture) {
snapshots[folder] = try snapshot(fixture, folder)
}
func step(_ label: String, targeting targets: Set<String>, _ operation: () throws -> Void) throws {
try operation()
for (folder, before) in snapshots where !targets.contains(folder) {
let after = try snapshot(fixture, folder)
#expect(after.data == before.data, "\(label) touched \(folder)'s bytes")
#expect(after.modified == before.modified, "\(label) touched \(folder)'s mtime")
}
for target in targets {
let before = try #require(snapshots[target])
let after = try snapshot(fixture, target)
#expect(after.data != before.data, "\(label) left \(target) byte-identical")
snapshots[target] = after
}
for folder in allIndexFolders(fixture) where snapshots[folder] == nil {
snapshots[folder] = try snapshot(fixture, folder)
}
}
try step("reorder", targeting: ["\(Ident.lane1)/\(Ident.card1)"]) {
try BoardWriter.updateIndex(
inItemFolder: fixture.url("\(Ident.lane1)/\(Ident.card1)"), operation: "reorder card"
) { $0.set(FrontmatterKeys.order, to: .double(1536)) }
}
try step("style write", targeting: ["\(Ident.lane1)/\(Ident.card2)"]) {
try BoardWriter.updateIndex(
inItemFolder: fixture.url("\(Ident.lane1)/\(Ident.card2)"), operation: "set background"
) { $0.set(FrontmatterKeys.background, to: .string("blue")) }
}
try step("rename", targeting: ["\(Ident.lane2)/\(Ident.card3)"]) {
try BoardWriter.updateIndex(
inItemFolder: fixture.url("\(Ident.lane2)/\(Ident.card3)"), operation: "rename card"
) { $0.set(FrontmatterKeys.title, to: .string("Renamed Three")) }
}
try step("delete", targeting: ["\(Ident.lane2)/\(Ident.card4)"]) {
try BoardWriter.deleteItem(at: fixture.url("\(Ident.lane2)/\(Ident.card4)"))
}
try step("restore", targeting: ["\(Ident.lane2)/\(Ident.card4)"]) {
try BoardWriter.restoreItem(at: fixture.url("\(Ident.lane2)/\(Ident.card4)"))
}
var createdCard = ""
try step("create card", targeting: []) {
createdCard = try BoardWriter.createCard(inLane: fixture.url(Ident.lane1), title: "Fresh").rawValue
}
#expect(BoardLoader.isUUIDShaped(createdCard))
try step("import attachment", targeting: []) {
// Dot-prefixed so it never becomes a board-root stray the loader has to warn about
// the source lives outside the board tree in spirit, just not in path.
let source = try fixture.file(".sources/shot.png", Data([0x01, 0x02]))
_ = try BoardWriter.importAttachments([source], intoCard: fixture.url("\(Ident.lane1)/\(Ident.card1)"))
}
#expect(try BoardLoader.load(boardRoot: fixture.root).warnings.isEmpty)
}
}
// MARK: - The renumber exception, stated positively
/// 01-storage-format.md § Ordering, "the one exception to the only-the-moved-item rule":
/// `renumberVisibleChildren` rewrites *every* visible sibling in the lane it renumbers bytes
/// and mtimes both move while everything outside that lane, tombstones inside it included,
/// stays put. `BoardWriterRenumberTests` already pins the within-lane byte half (tombstones and
/// strays untouched); this test widens the lens to the whole board and adds the mtime half on
/// both sides of the exception.
struct WriteFidelityRenumberTests {
@Test func renumberRewritesTheWholeLaneButLeavesEverythingElseByteAndMtimeIdentical() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1.0000003", title: "A"))
try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "1.0000001", title: "B"))
try fixture.item("\(Ident.lane1)/\(Ident.card3)", Item.rich(order: "1.0000002", title: "C"))
try fixture.item(
"\(Ident.lane1)/\(Ident.card4)",
"---\nschema: 1\norder: 0.5\ntitle: Gone\ndeleted: 2026-01-01T00:00:00Z\n---\nbody\n"
)
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
try fixture.item("\(Ident.lane2)/\(MoreIdent.card5)", Item.rich(order: "1024", title: "Other Lane's Card"))
let untouched = ["", Ident.lane1, Ident.lane2, "\(Ident.lane2)/\(MoreIdent.card5)", "\(Ident.lane1)/\(Ident.card4)"]
let renumbered = ["\(Ident.lane1)/\(Ident.card1)", "\(Ident.lane1)/\(Ident.card2)", "\(Ident.lane1)/\(Ident.card3)"]
var before: [String: (data: Data, modified: Date)] = [:]
for path in untouched + renumbered { before[path] = try snapshot(fixture, path) }
try BoardWriter.renumberVisibleChildren(of: fixture.url(Ident.lane1))
for path in untouched {
let previous = try #require(before[path])
let after = try snapshot(fixture, path)
#expect(after.data == previous.data)
#expect(after.modified == previous.modified)
}
for path in renumbered {
let previous = try #require(before[path])
let after = try snapshot(fixture, path)
#expect(after.data != previous.data)
#expect(after.modified != previous.modified)
}
let orders = try [Ident.card2, Ident.card3, Ident.card1].map {
try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\($0)")).order
}
#expect(orders == [.valid(1024), .valid(2048), .valid(3072)])
}
}
// MARK: - Delete restore, byte precision
/// 01-storage-format.md § Deletion: Put Back "undoes exactly what `deleteItem` wrote". Existing
/// coverage (`BoardWriterDeleteRestoreTests`) checks the *result* undeleted, reordered
/// correctly this test checks the *bytes*: after a full deleterestore round trip, the file
/// differs from the pre-delete original in exactly one place, the `modified:` line, with every
/// comment, inline comment, unknown key, and per-line ending untouched, and zero occurrences of
/// `deleted` anywhere in the text.
struct WriteFidelityTombstoneTests {
@Test func deleteThenRestoreDiffersFromTheOriginalOnlyInTheModifiedTimestamp() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Lane"))
let original = "---\n"
+ "schema: 1\n"
+ "# a hand-written note\n"
+ "title: Original\n"
+ "order: 1536\n"
+ "project: lanework # agent overlay\n"
+ "sphere: work\r\n"
+ "labels: [a, b, c]\n"
+ "modified: 2026-01-01T00:00:00Z\n"
+ "---\n"
+ "Body text.\n\nMore body — with *markdown*.\n"
let cardPath = "\(Ident.lane1)/\(Ident.card1)"
let folder = try fixture.item(cardPath, original)
try fixture.item("\(Ident.lane1)/\(Ident.card2)", "---\nschema: 1\norder: 2048\ntitle: Sibling\n---\n")
try BoardWriter.deleteItem(at: folder)
#expect(try FrontmatterDocument.parse(fixture.indexText(cardPath)).deleted.value != nil)
try BoardWriter.restoreItem(at: folder)
let after = try fixture.indexText(cardPath)
#expect(lines(of: after, excludingKeys: [FrontmatterKeys.modified])
== lines(of: original, excludingKeys: [FrontmatterKeys.modified]))
// The untouched CRLF unknown-key line and the inline comment both survived verbatim.
#expect(after.contains("sphere: work\r\n"))
#expect(after.contains("project: lanework # agent overlay\n"))
// No residue of the key that made this item a tombstone, anywhere in the text.
#expect(!after.contains("deleted"))
let document = try FrontmatterDocument.parse(after)
#expect(document.deleted == .missing)
#expect(document.order == .valid(1536))
#expect(document.title == .valid("Original"))
// Position among siblings unchanged: the loader sees the card back at its recorded
// order, ahead of the sibling that was never touched.
let result = try BoardLoader.load(boardRoot: fixture.root)
#expect(result.warnings.isEmpty)
let cards = try #require(result.model.lanes.first?.cards)
#expect(cards.map(\.id.rawValue) == [Ident.card1, Ident.card2])
#expect(cards[0].isDeleted == false)
#expect(cards[0].order == 1536)
}
}
// MARK: - Unknown-key order through a writer op
/// 01-storage-format.md § Fractal layout Rules, "unknown frontmatter keys and their order are
/// preserved verbatim on every rewrite" load-bearing for agent overlays (08-agent-integration.md).
/// `BoardWriterPreservationTests.aTitleEditTouchesOnlyTheTitleAndTheStamps` already pins this for
/// three unknown keys grouped contiguously; this test strengthens it with five, deliberately
/// interleaved among the schema-owned keys rather than clustered, so the guarantee is about
/// *document order*, not merely "the unknown-keys subsequence".
struct WriteFidelityUnknownKeyOrderTests {
@Test func interleavedUnknownKeysKeepTheirSequenceThroughATitleRewrite() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let original = """
---
schema: 1
alpha: 1
title: Before
beta: 2
order: 1024
gamma: 3
created: 2026-01-01T00:00:00Z
delta: 4
modified: 2026-01-01T00:00:00Z
epsilon: 5
---
Body.
"""
let folder = try fixture.item("card", original)
try BoardWriter.updateIndex(inItemFolder: folder, operation: "rename card") { document in
document.set(FrontmatterKeys.title, to: .string("After"))
}
let after = try fixture.indexText("card")
let excluded = [FrontmatterKeys.title, FrontmatterKeys.modified, FrontmatterKeys.modifiedBy]
#expect(lines(of: after, excludingKeys: excluded) == lines(of: original, excludingKeys: excluded))
let document = try FrontmatterDocument.parse(after)
#expect(document.unknownFields.map(\.key) == ["alpha", "beta", "gamma", "delta", "epsilon"])
#expect(document.keys == [
"schema", "alpha", "title", "beta", "order", "gamma", "created", "delta", "modified", "epsilon",
])
#expect(document.title == .valid("After"))
}
}
// MARK: - Cross-cutting composite
/// The "a brand-new board is indistinguishable from a hand-edited one" claim, composed into one
/// session: a board built purely through `BoardWriter`, one file hand-edited outside it entirely
/// (simulating an agent or a human in a text editor), then moved, copied, tombstoned, restored,
/// and renumbered and at the end `BoardLoader.load` succeeds with zero warnings, the hand-added
/// unknown keys survived, and every title and body reads back exactly as set.
struct WriteFidelityCompositeTests {
@Test func aBoardBuiltThroughTheWriterAndThenHandEditedLoadsCleanlyAfterEveryOperation() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let root = fixture.url("Composite.kanban")
try BoardWriter.createBoard(at: root, title: "Composite")
let lane1 = try BoardWriter.createLane(inBoard: root, title: "Todo")
let lane2 = try BoardWriter.createLane(inBoard: root, title: "Doing")
let lane1Folder = root.appendingPathComponent(lane1.rawValue, isDirectory: true)
let lane2Folder = root.appendingPathComponent(lane2.rawValue, isDirectory: true)
let card1 = try BoardWriter.createCard(inLane: lane1Folder, title: "First")
let card2 = try BoardWriter.createCard(inLane: lane1Folder, title: "Second")
let card3 = try BoardWriter.createCard(inLane: lane1Folder, title: "Third")
for (id, body) in [(card1, "First body.\n"), (card2, "Second body.\n"), (card3, "Third body.\n")] {
try BoardWriter.updateIndex(
inItemFolder: lane1Folder.appendingPathComponent(id.rawValue), operation: "edit body"
) { $0.body = body }
}
// A hand edit: an agent opens Second's file directly and adds unknown keys plus a
// comment no `BoardWriter` call involved, exactly what "indistinguishable" has to
// survive everything that follows.
let card2Path = "Composite.kanban/\(lane1.rawValue)/\(card2.rawValue)"
let handEdited = try fixture.indexText(card2Path).replacingOccurrences(
of: "modified:",
with: "project: lanework\nsphere: work # agent overlay\n# a note\nmodified:"
)
try fixture.item(card2Path, handEdited)
// Move: First crosses into Doing same board, so identity travels unchanged.
let moveResult = try BoardWriter.moveItem(
at: lane1Folder.appendingPathComponent(card1.rawValue),
toParent: lane2Folder,
sourceBoardRoot: root,
destinationBoardRoot: root,
order: nil
)
#expect(moveResult.id == card1)
#expect(moveResult.reminted.isEmpty)
// Copy: Third forks into Doing alongside it a new identity, the original left in Todo.
let copyID = try BoardWriter.copyItem(
at: lane1Folder.appendingPathComponent(card3.rawValue), toParent: lane2Folder, order: nil, stamps: .fork
)
#expect(copyID != card3)
// Delete, then restore, the hand-edited card its unknown keys and comment must come
// back with no residue of `deleted`.
try BoardWriter.deleteItem(at: lane1Folder.appendingPathComponent(card2.rawValue))
try BoardWriter.restoreItem(at: lane1Folder.appendingPathComponent(card2.rawValue))
// Renumber both lanes the fallback that touches every visible sibling's `order`.
try BoardWriter.renumberVisibleChildren(of: lane1Folder)
try BoardWriter.renumberVisibleChildren(of: lane2Folder)
let result = try BoardLoader.load(boardRoot: root)
#expect(result.warnings.isEmpty)
#expect(result.model.title == .valid("Composite"))
let lanes = Dictionary(uniqueKeysWithValues: result.model.lanes.map { ($0.id, $0) })
let todo = try #require(lanes[lane1])
let doing = try #require(lanes[lane2])
// Todo kept the two cards that never left: Second (hand-edited, survived a full
// delete/restore round trip) and Third (untouched original, still resident after being
// only *copied*, never moved).
#expect(Set(todo.cards.map(\.id)) == [card2, card3])
let restoredSecond = try #require(todo.cards.first { $0.id == card2 })
#expect(restoredSecond.title == .valid("Second"))
#expect(restoredSecond.body == "Second body.\n")
#expect(restoredSecond.isDeleted == false)
#expect(restoredSecond.document.unknownFields.map(\.key) == ["project", "sphere"])
#expect(restoredSecond.document.rawValue(for: "project") == "lanework")
let untouchedThird = try #require(todo.cards.first { $0.id == card3 })
#expect(untouchedThird.title == .valid("Third"))
#expect(untouchedThird.body == "Third body.\n")
// Doing holds the mover (original identity) and the fork (a fresh one).
#expect(Set(doing.cards.map(\.id)) == [card1, copyID])
let movedFirst = try #require(doing.cards.first { $0.id == card1 })
#expect(movedFirst.title == .valid("First"))
#expect(movedFirst.body == "First body.\n")
let forkedThird = try #require(doing.cards.first { $0.id == copyID })
#expect(forkedThird.title == .valid("Third"))
#expect(forkedThird.body == "Third body.\n")
}
}
+157
View File
@@ -0,0 +1,157 @@
import Foundation
import Testing
@testable import Kanban
/// Test helpers shared by `BoardWriterTests.swift` (per-operation coverage) and
/// `WriteFidelityTests.swift` (the cross-cutting write-path fidelity guarantees this file's
/// twin exists to pin) promoted out of `BoardWriterTests.swift`, `internal` rather than
/// `private`, the moment a second file needed them. Everything here writes and reads raw bytes
/// on disk, never through the app's own read path, so every assertion built on top of it is
/// about what is actually on disk (02-architecture.md § Layering Components, "a write is done
/// when the file is on disk").
// MARK: - Fixture
/// A temp directory holding hand-written `index.md` files, written and read back as raw bytes.
struct WriterFixture {
let root: URL
init() throws {
root = FileManager.default.temporaryDirectory
.appendingPathComponent("BoardWriterTests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
}
/// Restores permissions before removing: the atomicity test deliberately makes a folder
/// unwritable, and an unwritable folder is also an unremovable one.
func tearDown() {
let manager = FileManager.default
if let walker = manager.enumerator(atPath: root.path) {
for case let relative as String in walker {
try? manager.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: root.appendingPathComponent(relative).path
)
}
}
try? manager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: root.path)
try? manager.removeItem(at: root)
}
func url(_ relativePath: String) -> URL {
relativePath.isEmpty ? root : root.appendingPathComponent(relativePath, isDirectory: true)
}
/// Writes `text` verbatim (BOM-less UTF-8, line endings exactly as given) to
/// `<relativePath>/index.md`.
@discardableResult
func item(_ relativePath: String, _ text: String) throws -> URL {
try write(Data(text.utf8), to: relativePath)
}
@discardableResult
func item(_ relativePath: String, bytes: Data) throws -> URL {
try write(bytes, to: relativePath)
}
@discardableResult
private func write(_ data: Data, to relativePath: String) throws -> URL {
let folder = url(relativePath)
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
try data.write(to: folder.appendingPathComponent("index.md"))
return folder
}
/// Writes an arbitrary file not an `index.md` creating its folder: attachments and
/// strays, the content a copy has to carry verbatim without ever reading it.
@discardableResult
func file(_ relativePath: String, _ bytes: Data) throws -> URL {
let fileURL = root.appendingPathComponent(relativePath)
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try bytes.write(to: fileURL)
return fileURL
}
func data(_ relativePath: String) throws -> Data {
try Data(contentsOf: root.appendingPathComponent(relativePath))
}
func exists(_ relativePath: String) -> Bool {
FileManager.default.fileExists(atPath: url(relativePath).path)
}
func indexData(_ relativePath: String) throws -> Data {
try Data(contentsOf: url(relativePath).appendingPathComponent("index.md"))
}
func indexText(_ relativePath: String) throws -> String {
try String(decoding: indexData(relativePath), as: UTF8.self)
}
/// Every entry in the folder, hidden ones included the writer's temp files are hidden, so
/// only a listing that sees them can prove there is no residue.
func entryNames(_ relativePath: String) throws -> [String] {
try FileManager.default.contentsOfDirectory(atPath: url(relativePath).path).sorted()
}
}
// MARK: - Move/copy identities
/// Literal UUID-shaped names for the move/copy suites, which need more of them than
/// `BoardWriterTests.swift`'s own `Child` offers an import-boundary test has the *same*
/// identity living in two boards at once, and a compound arrival needs a lane with several
/// cards.
enum Ident {
static let lane1 = "11111111-1111-4111-8111-111111111111"
static let lane2 = "22222222-2222-4222-8222-222222222222"
static let lane3 = "33333333-3333-4333-8333-333333333333"
static let lane4 = "44444444-4444-4444-8444-444444444444"
static let card1 = "55555555-5555-4555-8555-555555555555"
static let card2 = "66666666-6666-4666-8666-666666666666"
static let card3 = "77777777-7777-4777-8777-777777777777"
static let card4 = "99999999-9999-4999-8999-999999999999"
static let indexless = "88888888-8888-4888-8888-888888888888"
}
/// The `index.md` texts the move/copy suites move and copy around.
enum Item {
static let board = "---\nschema: 1\ntitle: Board\n---\nBoard description.\n"
/// Everything a move or a copy has to leave alone: unknown keys with an inline comment, a
/// `created` stamp from before today, a foreign `modified-by`, and a body.
static func rich(order: String, title: String) -> String {
"""
---
schema: 1
title: \(title)
order: \(order)
project: lanework # agent overlay
labels: [a, b, c]
created: 2026-01-01T09:00:00Z
modified: 2026-02-02T09:00:00Z
modified-by: claude
---
\(title) body — with *markdown*.
"""
}
/// A whole-frontmatter flow mapping carrying a `modified-by`: readable, uneditable, and so
/// left byte-verbatim by a copy stale attribution included.
static let uneditable = "---\n{schema: 1, order: 1024, title: Odd, modified-by: claude}\n---\nodd body\n"
}
// MARK: - Failure assertion
func writeFailure(_ operation: () throws -> Void) -> BoardWriteError? {
do {
try operation()
Issue.record("expected the write to fail, but it succeeded")
return nil
} catch let error as BoardWriteError {
return error
} catch {
Issue.record("expected a BoardWriteError, got \(error)")
return nil
}
}