Files
lanework/KanbanTests/WriteFidelityTests.swift
rzen bec75e4282 Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires
(Ranks.isOrderedForTrash one comparator, loader + merged order agree;
the legacy deleted: migration stamps modified from the tombstone
timestamp where parseable; delete undo steps validate existence-only;
agent guide v8). Trash selection goes kind-blind — ranges, marquee,
Select All, and the successor walk sweep both kinds; the guard moves to
the exits (mixed-payload drop refusal, copy/cut validation). The copy
stamping preflight widens back to comment depth (load-scoped posture —
the board always loads, the gesture refuses whole). Fixes a latent
no-op: trashed-lane drag restore never fired (DragSession.beginLanes
hard-coded the board container).

2403 tests in 413 suites green.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-31 18:35:07 -04:00

639 lines
34 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `<root>/.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<String>,
departed: Set<String> = [],
_ 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
)
}
// 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))
// A **lane** delete is the same move, one level up (03 § Trash, re-ruled 2026-07-29): the
// lane's folder leaves the visible tree carrying its whole subtree, and the cards inside it
// are neither read nor rewritten — their mtimes prove it, since they are still tracked here
// under their new path only after the restore puts the lane back.
try step(
"delete lane",
targeting: [],
departed: [Ident.lane2, "\(Ident.lane2)/\(Ident.card3)", "\(Ident.lane2)/\(Ident.card4)"]
) {
try BoardWriter.deleteLaneToTrash(
at: fixture.url(Ident.lane2), inBoard: fixture.root
)
}
try step("restore lane", targeting: []) {
_ = try BoardWriter.moveItem(
at: BoardWriter.trashFolder(inBoard: fixture.root).appendingPathComponent(Ident.lane2),
toParent: fixture.root,
sourceBoardRoot: fixture.root,
destinationBoardRoot: fixture.root,
order: 2048
)
}
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
)
_ = 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
)
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 lane's trash move is the same container change, and its **subtree is not** — the nested
/// card is neither read nor rewritten, so its stamps are exactly what they were.
@Test("A lane's trash move stamps the lane and nothing beneath it")
func aLaneTrashMoveStampsOnlyTheLane() throws {
let fixture = try twoLaneBoard()
defer { fixture.tearDown() }
try BoardWriter.deleteLaneToTrash(
at: fixture.url(Ident.lane1), inBoard: fixture.root
)
let lane = try stamps(fixture, ".trash/\(Ident.lane1)")
#expect(lane.modified != Self.priorModified, "into the trash is a container change")
#expect(lane.modifiedBy == nil)
let card = try stamps(fixture, ".trash/\(Ident.lane1)/\(Ident.card1)")
#expect(card.modified == Self.priorModified, "the freight's own container did not change")
#expect(card.modifiedBy == "claude", "so even a foreign stamp survives, untouched")
}
/// 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")
}
}
}