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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user