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; 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) } /// `departed` names folders the step moved *out of the enumerated tree* — which, since /// `allIndexFolders` skips hidden folders, is exactly what a delete now is: the card's folder /// travels into `/.trash/` (03-board-ui.md § Trash, resettled 2026-07-28) and stops /// being visible here. They leave the tracked set rather than being asserted about, because /// "untouched" is a claim about the files that stayed. func step( _ label: String, targeting targets: Set, departed: Set = [], _ operation: () throws -> Void ) throws { try operation() for folder in departed { snapshots[folder] = nil } 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: .style(title: nil) ) { $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: .style(title: nil) ) { $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: .style(title: nil) ) { $0.set(FrontmatterKeys.title, to: .string("Renamed Three")) } } // The delete is a **move** into `.trash/` (03-board-ui.md § Trash): the card's folder leaves // the visible tree entirely, and every index that stayed behind must be untouched — mtime // included, which is the point of this harness. try step("delete", targeting: [], departed: ["\(Ident.lane2)/\(Ident.card4)"]) { try BoardWriter.deleteCardToTrash( at: fixture.url("\(Ident.lane2)/\(Ident.card4)"), inBoard: fixture.root, order: 1024 ) } // The restore is an ordinary move out — "there is no restore-specific machinery and no Put // Back" (03 § Trash) — so the folder simply comes back, and again nothing else moves. try step("restore", targeting: []) { _ = try BoardWriter.moveItem( at: BoardWriter.trashFolder(inBoard: fixture.root).appendingPathComponent(Ident.card4), toParent: fixture.url(Ident.lane2), sourceBoardRoot: fixture.root, destinationBoardRoot: fixture.root, order: 2048 ) } 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: - 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: .style(title: nil)) { 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: .style(title: nil) ) { $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 — two folder moves now (03-board-ui.md § Trash, // resettled 2026-07-28), so its unknown keys, its comment and its body must ride along // untouched through both legs. try BoardWriter.deleteCardToTrash( at: lane1Folder.appendingPathComponent(card2.rawValue), inBoard: root, order: 1024 ) _ = try BoardWriter.moveItem( at: BoardWriter.trashFolder(inBoard: root).appendingPathComponent(card2.rawValue), toParent: lane1Folder, sourceBoardRoot: root, destinationBoardRoot: root, order: nil ) // 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") } } // MARK: - The container-change predicate /// **01-storage-format.md § Frontmatter ▸ `modified`'s scope** — ruled 2026-07-29 as /// moves-don't-stamp, **refined 2026-07-30** to one container-change predicate: /// /// > a reorder within the item's container (a card among its lane's siblings, a lane among the /// > board's lanes) and a renumber's whole-lane rescale rewrite `index.md` without touching content: /// > no stamp, and no `modified-by` clear … **A move that changes the item's container stamps both**: /// > a cross-lane move, a cross-board arrival, and the trash move. /// /// The pairing is the thing these tests are really pinning: `modified` and `modified-by` move /// together, always, because "attribution can't change when content didn't". So every case below /// asserts both keys, and the fixtures deliberately carry a foreign `modified-by: claude` — the key /// whose survival is the only visible difference between an order-only rewrite and a content one. /// /// **There is deliberately no trash case in the implementation**, and that is what /// `theTrashMoveStampsBecauseEveryContainerChangeDoes` exists to state from the outside: the trash /// move stamps, and it does so through the same predicate as a cross-lane move rather than through a /// branch of its own. struct WriteFidelityStampingTests { /// The prior stamps every fixture below starts from — `Item.rich`'s own, so a test asserting /// "unchanged" is asserting against a real value that a stamp would visibly replace. private static let priorModified = "2026-02-02T09:00:00Z" private func stamps(_ fixture: WriterFixture, _ path: String) throws -> (modified: String?, modifiedBy: String?) { let document = try FrontmatterDocument.parse(fixture.indexText(path)) return (document.rawValue(for: FrontmatterKeys.modified), document.rawValue(for: FrontmatterKeys.modifiedBy)) } private func twoLaneBoard() throws -> WriterFixture { let fixture = try WriterFixture() 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: "First")) try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing")) return fixture } /// A card dropped back into its own lane — `moveItem`'s same-parent degenerate path, which is /// every within-lane drag, every ⌥⌘↑/↓ sort step, and every inverse of one. @Test("A card reordered among its lane's siblings rewrites only order") func aWithinLaneReorderRewritesOnlyOrder() throws { let fixture = try twoLaneBoard() defer { fixture.tearDown() } _ = try BoardWriter.moveItem( at: fixture.url("\(Ident.lane1)/\(Ident.card1)"), toParent: fixture.url(Ident.lane1), sourceBoardRoot: fixture.root, destinationBoardRoot: fixture.root, order: 3072 ) let after = try stamps(fixture, "\(Ident.lane1)/\(Ident.card1)") #expect(after.modified == Self.priorModified, "a reorder is not a content write") #expect(after.modifiedBy == "claude", "and attribution can't change when content didn't") let document = try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(Ident.card1)")) #expect(document.order.value == 3072, "the one key a reorder owns did move") } /// A lane's parent is the board root and nothing else, so *every* lane reorder is /// within-container — ⌘←/⌘→, the strip drag, and their inverses alike. @Test("A lane reordered on the board rewrites only order") func aLaneReorderRewritesOnlyOrder() throws { let fixture = try twoLaneBoard() defer { fixture.tearDown() } _ = try BoardWriter.moveItem( at: fixture.url(Ident.lane2), toParent: fixture.root, sourceBoardRoot: fixture.root, destinationBoardRoot: fixture.root, order: 512 ) let after = try stamps(fixture, Ident.lane2) #expect(after.modified == Self.priorModified) #expect(after.modifiedBy == "claude") #expect(try FrontmatterDocument.parse(fixture.indexText(Ident.lane2)).order.value == 512) } /// The renumber rescale — 01 § Ordering, verbatim: "order-only rewrites, so no `modified` stamp /// and no `modified-by` clear". Every sibling in the lane is rewritten, and not one of them is /// stamped, which is what keeps a midpoint exhaustion from reading as a lane's worth of edits. @Test("A renumber rescale stamps nothing, on any sibling") func aRenumberRescaleStampsNothing() throws { let fixture = try twoLaneBoard() defer { fixture.tearDown() } try BoardWriter.renumberVisibleChildren(of: fixture.url(Ident.lane1)) for path in ["\(Ident.lane1)/\(Ident.card1)", "\(Ident.lane1)/\(Ident.card2)"] { let after = try stamps(fixture, path) #expect(after.modified == Self.priorModified, "\(path) was stamped by a rescale") #expect(after.modifiedBy == "claude", "\(path) lost its attribution to a rescale") } #expect(try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(Ident.card1)")).order.value == 1024) #expect(try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(Ident.card2)")).order.value == 2048) } /// The other side of the predicate: which lane a card lives in is *state*, so crossing lanes is a /// content write and stamps both keys. @Test("A cross-lane move stamps modified and clears modified-by") func aCrossLaneMoveStamps() throws { let fixture = try twoLaneBoard() defer { fixture.tearDown() } _ = try BoardWriter.moveItem( at: fixture.url("\(Ident.lane1)/\(Ident.card1)"), toParent: fixture.url(Ident.lane2), sourceBoardRoot: fixture.root, destinationBoardRoot: fixture.root, order: 1024 ) let after = try stamps(fixture, "\(Ident.lane2)/\(Ident.card1)") #expect(after.modified != Self.priorModified, "a container change is a content write") #expect(after.modifiedBy == nil, "and clears the foreign stamp like any app write") } /// **No trash special case anywhere.** The delete stamps, the restore stamps, and both do it /// through the container predicate rather than through a rule of their own — which is why this /// test asserts the same two facts as `aCrossLaneMoveStamps` and nothing extra. @Test("The trash move stamps because every container change does — in and out") func theTrashMoveStampsBecauseEveryContainerChangeDoes() throws { let fixture = try twoLaneBoard() defer { fixture.tearDown() } try BoardWriter.deleteCardToTrash( at: fixture.url("\(Ident.lane1)/\(Ident.card1)"), inBoard: fixture.root, order: 1024 ) let trashed = try stamps(fixture, ".trash/\(Ident.card1)") #expect(trashed.modified != Self.priorModified, "into the trash is a container change") #expect(trashed.modifiedBy == nil) // And out again. `modified-by` is re-planted by hand first, standing in for the agent that // re-stamped the card while it sat in the trash: the restore has to clear it again. try BoardWriter.updateIndex( inItemFolder: fixture.url(".trash/\(Ident.card1)"), operation: .style(title: nil) ) { $0.set(FrontmatterKeys.modifiedBy, to: .string("claude")) } _ = try BoardWriter.moveItem( at: fixture.url(".trash/\(Ident.card1)"), toParent: fixture.url(Ident.lane2), sourceBoardRoot: fixture.root, destinationBoardRoot: fixture.root, order: 4096 ) let restored = try stamps(fixture, "\(Ident.lane2)/\(Ident.card1)") #expect(restored.modifiedBy == nil, "out of the trash is a container change too") } /// A cross-board arrival changes the container as surely as a cross-lane move does, and the /// import boundary's remint does not change that: the arrived file is stamped either way. @Test("A cross-board arrival stamps") func aCrossBoardArrivalStamps() throws { let source = try twoLaneBoard() defer { source.tearDown() } let destination = try WriterFixture() defer { destination.tearDown() } try destination.item("", Item.board) try destination.item(Ident.lane3, Item.rich(order: "1024", title: "Elsewhere")) let result = try BoardWriter.moveItem( at: source.url("\(Ident.lane1)/\(Ident.card1)"), toParent: destination.url(Ident.lane3), sourceBoardRoot: source.root, destinationBoardRoot: destination.root, order: 1024 ) let after = try stamps(destination, "\(Ident.lane3)/\(result.id.rawValue)") #expect(after.modified != Self.priorModified) #expect(after.modifiedBy == nil) } /// The predicate as a pure value — one exhaustive statement of which operations are order-only, /// so a new `WriteOperation` cannot quietly join or leave the class. **`.reorder` and /// `.renumberChildren`, and nothing else**; `.delete` and `.move` are named explicitly because /// they are the two a "moves don't stamp" reading would have put on the wrong side. @Test("Only reorder and renumber are order-only") func theOrderOnlyClassIsExactlyTwoOperations() { #expect(WriteOperation.reorder(title: nil).rewritesOrderOnly) #expect(WriteOperation.renumberChildren.rewritesOrderOnly) for operation: WriteOperation in [ .createBoard, .createLane, .createCard, .move(title: nil), .copy(title: nil), .paste(title: nil), .delete(title: nil), .purge(title: nil), .migrateTombstone(title: nil), .style(title: nil), .resize(title: nil), .rename(title: nil), .duplicateBoard(title: nil), .saveAsTemplate(title: nil), .importAttachment(filename: "a"), .listAttachments, .removeAttachment(filename: "a"), .relocateLooseFile(filename: "a"), .agentGuide, .displaceClaimedName(name: ".trash"), .repairDuplicateID(title: nil), .toggleTask(title: nil), .editBody(title: nil), .rawSource(title: nil), ] { #expect(operation.rewritesOrderOnly == false, "\(operation) should be a content write") } } }