import Foundation import Testing @testable import Kanban /// **Duplicate ids within a board are never tolerated** (01-storage-format.md § Fractal layout ▸ /// Rules, settled; the silent-remint re-ruling of 2026-07-29). /// /// Two folders can end up carrying one UUID however carefully the app mints them — a user copies a /// card folder in Finder, an agent duplicates a tree, an archive is unpacked over a board. The /// snapshot invariant does not bend for any of it: **one occurrence per id, board-wide**, because /// SwiftUI's `ForEach` does not tolerate two equal ids and a board that crashes on load is the worst /// failure a files-first app has. /// /// Two classes, two verdicts: /// /// - **Case-spelled twins** of one identity are spelling artifacts of the *same* item. The canonical /// all-lowercase spelling wins where present, else the lexicographically first; the loser is a /// silent stray — logged, preserved, never rendered, **never reminted** (reminting one would create /// duplicate content the user never made). /// - **Content duplicates** — distinct folders carrying the identical name string, necessarily under /// different parents — are copies. Earlier-occurrence-wins; every later occurrence is withheld from /// the snapshot and then **reminted by a scheduled heal** (re-ruled 2026-07-29 — the user-gated /// Repair banner retired: "Lanework owns the board and re-mints identity at will"). // MARK: - Identities /// UUIDs with hex *letters* in them, because a case-twin fixture needs a name whose spelling can /// actually change — `Ident.card1` is all digits and uppercases to itself. private enum Dup { /// The canonical, all-lowercase spelling. static let lower = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee" /// The same identity, shouted. static let upper = "AAAAAAAA-BBBB-4CCC-8DDD-EEEEEEEEEEEE" /// The same identity again, spelled a third way — and lexicographically *after* `upper`, because /// `"B" < "b"` in ASCII. static let mixed = "AAAAAAAA-bbbb-4ccc-8ddd-eeeeeeeeeeee" /// An unrelated identity, for boards that need a card that is nobody's twin. static let other = "fedcba98-7654-4321-8fed-cba987654321" } private func occurrence( _ path: String, _ name: String, container: IntegrityRules.IdentityOccurrence.Container = .live, title: String? = nil, birth: Date? = nil, historyRank: Int? = nil ) -> IntegrityRules.IdentityOccurrence { IntegrityRules.IdentityOccurrence( path: path, name: name, container: container, title: title, birth: birth, historyRank: historyRank ) } private func date(_ offset: TimeInterval) -> Date { Date(timeIntervalSince1970: 1_700_000_000 + offset) } // MARK: - The rule /// `IntegrityRules.dedupe(_:)` is pure — an occurrence list in, two lists of losers out — so the rule /// is pinned here without a filesystem in the way. @Suite("Duplicate ids ▸ the dedupe rule") struct DedupeRuleTests { @Test("A board with no repeated identity yields nothing") func nothingToDo() { let verdict = IntegrityRules.dedupe([ occurrence(Ident.lane1, Ident.lane1), occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower), occurrence("\(Ident.lane1)/\(Dup.other)", Dup.other), ]) #expect(verdict.duplicates.isEmpty) #expect(verdict.caseTwins.isEmpty) } /// The headline case: one hand copy, two lanes, identical name strings. @Test("Two identical spellings under different parents: the later one is withheld") func contentDuplicateWithholdsTheLater() { let verdict = IntegrityRules.dedupe([ occurrence(Ident.lane1, Ident.lane1), occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, title: "Fix login"), occurrence(Ident.lane2, Ident.lane2), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, title: "Fix login"), ]) #expect(verdict.caseTwins.isEmpty, "identical spellings are copies, never spelling artifacts") #expect(verdict.duplicates == [ DuplicateIdentity( path: "\(Ident.lane2)/\(Dup.lower)", identity: Dup.lower, title: "Fix login", winner: "\(Ident.lane1)/\(Dup.lower)" ), ]) } /// Rung two of the ladder outranks rung three: the older folder wins even when the traversal meets /// it second. @Test("Birth date beats traversal order") func birthDateBeatsTraversal() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(100)), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(0)), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"]) #expect(verdict.duplicates.map(\.winner) == ["\(Ident.lane2)/\(Dup.lower)"]) } @Test("Equal birth dates fall through to traversal order") func equalDatesFallThrough() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(0)), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(0)), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) } /// **An unreadable date is no comparison at all.** Substituting `.distantPast` for a missing birth /// date would silently make the unreadable folder win every comparison it entered. @Test("One unreadable birth date drops the comparison to traversal order") func oneMissingDateFallsThrough() { // The folder *with* the older date is second in traversal; if a `nil` date were read as // `.distantPast` the first would win, which is exactly what must not happen. let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: nil), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(-10_000)), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) } @Test("Neither date readable falls through to traversal order") func noDatesFallThrough() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) } /// Rung one — the git seam. Base never fills it; pro-m1 does. @Test("Git path history beats birth date") func historyBeatsBirthDate() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(0), historyRank: 7), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100), historyRank: 3), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"]) } /// "The path history already tracks outranks the newcomer" — read literally, and *against* the /// birth date, which is the whole reason the rungs are ordered. @Test("A tracked path outranks an untracked one whatever the dates say") func trackedOutranksUntracked() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(0), historyRank: nil), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100), historyRank: 9), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"]) } @Test("Equal history ranks fall through to the next rung") func equalRanksFallThrough() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(100), historyRank: 4), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(0), historyRank: 4), ]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"]) } // MARK: The container boundary — rung 0 /// **The visible card never loses to its own ghost** (01-storage-format.md § Fractal layout ▸ /// Rules, stated 2026-07-29): the straddle case is a restore done as a *copy* — an ⌥-drag out of /// the trash in Finder — where the ghost left behind is genuinely the older folder. Every /// age-based rung would withhold the card the user just restored; rung 0 is what stops them being /// consulted at all. @Test("A live occurrence beats an older trashed one") func liveBeatsOlderTrashed() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(1000)), occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)), ]) #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"]) #expect(verdict.duplicates.map(\.winner) == ["\(Ident.lane1)/\(Dup.lower)"]) } /// Rung 0 sits **above history too**, not merely above age: an ⌥-drag restore leaves the tracked /// path in the trash, so the ghost is both older *and* the one git knows. @Test("A live occurrence beats a trashed one git says entered first") func liveBeatsTrackedTrashed() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(1000), historyRank: nil), occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0), historyRank: 1), ]) #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"]) } /// Traversal order does not decide it either — a trashed occurrence met *first* still loses. @Test("A trashed occurrence met first still loses") func trashedMetFirstStillLoses() { let verdict = IntegrityRules.dedupe([ occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed), occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower), ]) #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"]) } /// **A group wholly inside the trash falls through to the ordinary ladder**, unchanged: rung 0 only /// speaks when the occurrences straddle the boundary. /// /// The paths here are labels — the flat trash cannot really hold two entries with one name, so this /// pins the *rule*'s fall-through rather than a reachable board. What makes it worth pinning is the /// mistake it rules out: a container rung implemented as "trashed always loses" rather than as a /// comparison would have no answer at all when both sides are trashed. @Test("Two trashed occurrences fall through to the age ladder") func bothTrashedFallThrough() { let verdict = IntegrityRules.dedupe([ occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(1000)), occurrence(".trash/nested/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)), ]) #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"], "the older trashed one wins") } /// Several live occurrences and a ghost: the ghost is out on rung 0, and the live ones settle it /// among themselves on the age rungs. @Test("The ghost is out first, then the live occurrences settle it by age") func ghostOutThenAgeDecides() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(200)), occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100)), ]) #expect(verdict.duplicates.map(\.path) == [ "\(Ident.lane1)/\(Dup.lower)", ".trash/\(Dup.lower)", ]) #expect(verdict.duplicates.map(\.winner) == Array(repeating: "\(Ident.lane2)/\(Dup.lower)", count: 2)) } // MARK: Case twins /// The canonical spelling wins **wherever it sits in the traversal** — this is not an /// earlier-occurrence question, it is a spelling question. @Test("The all-lowercase spelling wins, however late it is met") func lowercaseWins() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper, birth: date(0)), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100)), ]) #expect(verdict.duplicates.isEmpty, "a spelling artifact is never healed") #expect(verdict.caseTwins == [ CaseTwin(path: "\(Ident.lane1)/\(Dup.upper)", winner: "\(Ident.lane2)/\(Dup.lower)"), ]) } @Test("With no lowercase spelling present, the lexicographically first wins") func lexicographicallyFirstWins() { // "AAAAAAAA-BBBB-…" < "AAAAAAAA-bbbb-…" — uppercase sorts before lowercase in ASCII. let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.mixed)", Dup.mixed), occurrence("\(Ident.lane2)/\(Dup.upper)", Dup.upper), ]) #expect(verdict.duplicates.isEmpty) #expect(verdict.caseTwins == [ CaseTwin(path: "\(Ident.lane1)/\(Dup.mixed)", winner: "\(Ident.lane2)/\(Dup.upper)"), ]) } /// **Mixed groups collapse spelling first, then apply earlier-occurrence-wins to what is left.** /// The consequence is deliberate and documented: a copy whose case was *also* hand-changed /// degrades to the silent case-twin posture and is never reminted. @Test("Three occurrences spanning both classes: spelling collapses first") func mixedGroupCollapsesSpellingFirst() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, title: "Fix login", birth: date(0)), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, title: "Fix login", birth: date(100)), occurrence("\(Ident.lane3)/\(Dup.upper)", Dup.upper, title: "Fix login", birth: date(200)), ]) // The uppercase copy is a spelling artifact — silent, never reminted… #expect(verdict.caseTwins == [ CaseTwin(path: "\(Ident.lane3)/\(Dup.upper)", winner: "\(Ident.lane1)/\(Dup.lower)"), ]) // …and only the second *canonically spelled* occurrence is healed. #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) } /// The winner a case twin is named against is the earlier-occurrence winner, not merely the first /// canonically spelled folder the walk met. @Test("A case twin's named winner is the group's actual winner") func caseTwinNamesTheRealWinner() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100)), occurrence("\(Ident.lane3)/\(Dup.lower)", Dup.lower, birth: date(0)), ]) #expect(verdict.caseTwins.map(\.winner) == ["\(Ident.lane3)/\(Dup.lower)"]) #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) } /// **The spelling contest obeys the container preference too.** Without it the canonical-wins rule /// would hand the identity to the lowercase ghost in the trash and *skip* the live card as a /// spelling artifact — the straddle read backwards, and the one outcome the ruling forbids: the /// visible card would vanish while its ghost rendered from the trash. /// /// The twin stays a **twin**: skipped silently, preserved, never reminted. Only the winner /// selection gained the container preference; the spelling-artifacts-stay-silent rule is unchanged. @Test("An uppercase live occurrence beats a lowercase trashed twin") func liveSpellingBeatsTrashedCanonicalSpelling() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper, birth: date(1000)), occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)), ]) #expect(verdict.duplicates.isEmpty, "a spelling artifact is never reminted, trashed or not") #expect(verdict.caseTwins == [ CaseTwin(path: ".trash/\(Dup.lower)", winner: "\(Ident.lane1)/\(Dup.upper)"), ]) } /// Among *live* spellings the ordinary rule still decides — the container filter narrows the /// candidates, it does not replace canonical-else-lexicographic. @Test("Among live spellings, canonical still wins") func canonicalStillWinsAmongLiveSpellings() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper, birth: date(0)), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(1000)), occurrence(".trash/\(Dup.mixed)", Dup.mixed, container: .trashed, birth: date(-1000)), ]) #expect(verdict.duplicates.isEmpty) #expect(verdict.caseTwins.map(\.path).sorted() == [ ".trash/\(Dup.mixed)", "\(Ident.lane1)/\(Dup.upper)", ].sorted()) #expect(verdict.caseTwins.allSatisfy { $0.winner == "\(Ident.lane2)/\(Dup.lower)" }) } /// A wholly trashed group has no live candidates, so the spelling contest is exactly what it was. @Test("A wholly trashed group picks its spelling the old way") func whollyTrashedSpellingUnchanged() { let verdict = IntegrityRules.dedupe([ occurrence(".trash/\(Dup.upper)", Dup.upper, container: .trashed, birth: date(0)), occurrence(".trash/nested/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(1000)), ]) #expect(verdict.duplicates.isEmpty) #expect(verdict.caseTwins == [ CaseTwin(path: ".trash/\(Dup.upper)", winner: ".trash/nested/\(Dup.lower)"), ]) } // MARK: Determinism and shape /// **Exactly one survivor per identity, whatever the group looks like** — the invariant the whole /// pass exists for, over a deliberately nasty group of five. @Test("Every group keeps exactly one occurrence") func exactlyOneSurvivor() { let occurrences = [ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(50)), occurrence("\(Ident.lane2)/\(Dup.upper)", Dup.upper, birth: date(10)), occurrence("\(Ident.lane3)/\(Dup.lower)", Dup.lower, birth: date(20)), occurrence("\(Ident.lane4)/\(Dup.mixed)", Dup.mixed, birth: date(0)), occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(90)), ] let verdict = IntegrityRules.dedupe(occurrences) let lost = Set(verdict.duplicates.map(\.path)).union(verdict.caseTwins.map(\.path)) #expect(lost.count == 4) #expect(occurrences.filter { !lost.contains($0.path) }.map(\.path) == ["\(Ident.lane3)/\(Dup.lower)"]) } /// The output order is the traversal's, not a dictionary's — the notice's subjects and the log's /// lines must read in board order, and the same input must produce the same answer every time. @Test("Losers come back in traversal order, repeatably") func traversalOrderedAndStable() { let occurrences = [ occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower), occurrence("\(Ident.lane1)/\(Dup.other)", Dup.other), occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower), occurrence("\(Ident.lane2)/\(Dup.other)", Dup.other), occurrence("\(Ident.lane3)/\(Dup.lower)", Dup.lower), ] let expected = [ "\(Ident.lane2)/\(Dup.lower)", "\(Ident.lane2)/\(Dup.other)", "\(Ident.lane3)/\(Dup.lower)", ] for _ in 0..<20 { #expect(IntegrityRules.dedupe(occurrences).duplicates.map(\.path) == expected) } } /// Identity comparison is UUID-*value* equality, so an occurrence's `identity` is its name folded /// — and that is what the defect carries, never the verbatim spelling. @Test("A duplicate's identity is canonical, never the folder's spelling") func identityIsCanonical() { let verdict = IntegrityRules.dedupe([ occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper), occurrence("\(Ident.lane2)/\(Dup.upper)", Dup.upper), ]) #expect(verdict.duplicates.map(\.identity) == [Dup.lower]) } } // MARK: - Fixtures @MainActor private func makeBoard() 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.lane2, Item.rich(order: "2048", title: "Doing")) return fixture } /// Every identity the snapshot renders, canonically — lanes, their cards, and the trash. private func renderedIdentities(_ model: BoardModel) -> [String] { var identities: [String] = [] for lane in model.lanes { identities.append(IntegrityRules.canonicalIdentity(lane.id.rawValue)) identities.append(contentsOf: lane.cards.map { IntegrityRules.canonicalIdentity($0.id.rawValue) }) } identities.append(contentsOf: model.trash.map { IntegrityRules.canonicalIdentity($0.id.rawValue) }) return identities } private func setBirth(_ url: URL, _ when: Date) throws { try FileManager.default.setAttributes([.creationDate: when], ofItemAtPath: url.path) } @MainActor private final class BracketLog { private(set) var begins = 0 func attach(to store: BoardStore) { store.watcherBrackets = (begin: { self.begins += 1 }, end: {}) } } @MainActor private final class StepLog: HistoryProviding { var canUndo = false var canRedo = false var undoActionName: String? var redoActionName: String? private(set) var registered: [String] = [] func register(_ step: HistoryStep) { registered.append(step.name) } func undo() {} func redo() {} func clear() {} } // MARK: - Detection, through the real loader /// Detection is **read-only in the loader** — the walk reports and withholds, the store acts. @MainActor @Suite("Duplicate ids ▸ detection") struct DuplicateIdentityDetectionTests { @Test("A card hand-copied into another lane loads once, and the copy is reported") func copiedCardLoadsOnce() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(original, date(0)) try setBirth(copy, date(1000)) let result = try BoardLoader.load(boardRoot: fixture.root) // One occurrence per id, board-wide — the invariant. #expect(renderedIdentities(result.model).sorted() == renderedIdentities(result.model).sorted()) #expect(Set(renderedIdentities(result.model)).count == renderedIdentities(result.model).count) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.isEmpty == true) #expect(result.duplicateIdentities == [ DuplicateIdentity( path: "\(Ident.lane2)/\(Dup.lower)", identity: Dup.lower, title: "Fix login", winner: "\(Ident.lane1)/\(Dup.lower)" ), ]) // Nothing was moved, nothing was written: the loader never writes. #expect(fixture.exists("\(Ident.lane2)/\(Dup.lower)/index.md")) #expect(try fixture.indexText("\(Ident.lane2)/\(Dup.lower)") == Item.rich(order: "1024", title: "Fix login")) } /// A healthy board pays nothing for this pass and reports nothing — the gate. @Test("A board with no duplicates reports none") func healthyBoardIsQuiet() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try fixture.item("\(Ident.lane2)/\(Dup.other)", Item.rich(order: "1024", title: "Other")) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.defects.isEmpty) #expect(result.warnings.isEmpty) #expect(result.duplicateIdentities.isEmpty) } /// **Board-wide spans both containers**: a card in the trash and a card in a lane sharing one id /// would put two items with equal ids into one snapshot the moment either was rendered. @Test("The trash counts — a lane card and a trash card sharing an id dedupe") func trashCounts() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let trashed = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(live, date(0)) try setBirth(trashed, date(1000)) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.model.trash.isEmpty) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"]) // The trash's `kind` reading goes with the withheld entry: leaving it would stand as an answer // about the *live* card, which is the ambiguity the dedupe exists to remove. #expect(result.trashKinds.isEmpty) } /// **The restore-as-a-copy straddle, end to end** (01-storage-format.md § Fractal layout ▸ Rules, /// stated 2026-07-29): the user ⌥-dragged a card out of the trash in Finder, so the ghost left /// behind is the *older* folder. Earlier-entrant-wins alone would withhold the very card they just /// restored and render its ghost from the trash; the container boundary is the first tie-break /// precisely so that cannot happen. @Test("A live card outranks its own older ghost in the trash") func liveCardOutranksItsGhost() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(live, date(1000)) try setBirth(ghost, date(0)) let result = try BoardLoader.load(boardRoot: fixture.root) // The restored card renders; the ghost is the withheld one. #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) #expect(result.model.trash.isEmpty) #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"]) #expect(result.duplicateIdentities.map(\.winner) == ["\(Ident.lane1)/\(Dup.lower)"]) } /// The same straddle with git in the picture: an ⌥-drag restore leaves the *tracked* path in the /// trash, so the ghost is the one history knows — and still loses. @Test("A tracked ghost still loses to the untracked live card") func trackedGhostStillLoses() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(live, date(1000)) try setBirth(ghost, date(0)) let ranker = BoardLoader.IdentityHistoryRanker { path in path == ".trash/\(Dup.lower)" ? 1 : nil } let result = try BoardLoader.load(boardRoot: fixture.root, historyRanker: ranker) #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"]) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) } /// **The same preference governs a trashed lane sharing a live lane's UUID** — the ruling says so /// explicitly, and it needs no special case: a trashed lane is a `.trash/` entry like any other. @Test("A trashed lane loses to the live lane sharing its UUID") func trashedLaneLosesToLiveLane() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let live = try fixture.item(Dup.lower, Item.rich(order: "1024", title: "Todo")) // A trashed lane: a `.trash/` entry with identity-shaped children of its own. let trashed = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Todo")) try fixture.item(".trash/\(Dup.lower)/\(Dup.other)", Item.rich(order: "1024", title: "Nested")) try setBirth(live, date(1000)) try setBirth(trashed, date(0)) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.model.lanes.map { $0.id.rawValue } == [Dup.lower]) #expect(result.model.trash.isEmpty) #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"]) } /// The case-twin straddle through the real loader: the live card is spelled uppercase, its ghost /// canonically. The live card renders, the ghost is a **silent twin** — skipped, preserved, and /// never reminted, so no heal is scheduled at all. @Test("A live uppercase card outranks its canonically spelled ghost, silently") func liveUppercaseBeatsCanonicalGhost() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let live = try fixture.item("\(Ident.lane1)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login")) let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(live, date(1000)) try setBirth(ghost, date(0)) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) #expect(result.model.trash.isEmpty) #expect(result.defects.isEmpty, "a spelling artifact is never work") #expect(result.warnings == [ .caseTwinIgnored(path: ".trash/\(Dup.lower)", winner: "\(Ident.lane1)/\(Dup.upper)"), ]) #expect(fixture.exists(".trash/\(Dup.lower)/index.md"), "preserved verbatim") } /// **Levels are not identity namespaces.** A lane folder copied *into* a lane becomes a /// card-level folder carrying the lane's id — one identity, two folders, two levels. @Test("A lane and a card sharing an id dedupe across levels") func crossLevelDuplicate() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) let lane = try fixture.item(Dup.lower, Item.rich(order: "1024", title: "Todo")) try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing")) let nested = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Copied lane")) try setBirth(lane, date(0)) try setBirth(nested, date(1000)) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.model.lanes.map { $0.id.rawValue }.sorted() == [Dup.lower, Ident.lane2].sorted()) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.isEmpty == true) #expect(result.duplicateIdentities.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) } /// **A withheld lane takes its subtree out of the snapshot** — and its cards are still occurrences /// in their own right, so a collision nested inside a losing lane is its own withheld occurrence /// with its own remint (the import boundary's finest-grain rule, read for the heal). @Test("A withheld lane's cards are still their own occurrences") func withheldLaneSubtreeStillCollides() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", Item.board) // `/` (a lane) and `//` (a card) share an identity; the lane // loses, so its whole subtree leaves the snapshot. try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing")) let nested = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Copied lane")) let lane = try fixture.item(Dup.lower, Item.rich(order: "1024", title: "Todo")) try setBirth(nested, date(0)) try setBirth(lane, date(1000)) // The losing lane's own card collides with a card in the surviving lane. let insideLoser = try fixture.item("\(Dup.lower)/\(Dup.other)", Item.rich(order: "1024", title: "Nested")) let insideWinner = try fixture.item("\(Ident.lane2)/\(Dup.other)", Item.rich(order: "2048", title: "Nested")) try setBirth(insideWinner, date(0)) try setBirth(insideLoser, date(1000)) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.model.lanes.map { $0.id.rawValue } == [Ident.lane2]) // Both the losing lane *and* the collision nested inside it are withheld occurrences, each // with its own remint to come. #expect(Set(result.duplicateIdentities.map(\.path)) == [ Dup.lower, "\(Dup.lower)/\(Dup.other)", ]) } // MARK: Case twins /// Case twins are the **tolerate** tier: a warning, not a defect — nothing to heal. /// /// Placed under different parents deliberately: default APFS is case-insensitive, so two /// case-spelled *siblings* cannot exist there at all. @Test("A case-spelled twin is a silent stray, not a defect") func caseTwinIsSilent() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.item("\(Ident.lane1)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login")) try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.defects.isEmpty, "a spelling artifact is never work") #expect(result.warnings == [ .caseTwinIgnored(path: "\(Ident.lane1)/\(Dup.upper)", winner: "\(Ident.lane2)/\(Dup.lower)"), ]) // The canonical spelling renders; the twin is preserved verbatim and never rendered. #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.isEmpty == true) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 1) #expect(fixture.exists("\(Ident.lane1)/\(Dup.upper)/index.md")) } @Test("With no canonical spelling, the lexicographically first renders") func lexicographicWinnerRenders() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.item("\(Ident.lane1)/\(Dup.mixed)", Item.rich(order: "1024", title: "Fix login")) try fixture.item("\(Ident.lane2)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login")) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.defects.isEmpty) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 1) #expect(result.warnings.map(\.description) == [ "\(Ident.lane1)/\(Dup.mixed): case-spelled twin of \(Ident.lane2)/\(Dup.upper), ignored as a spelling artifact", ]) } // MARK: The history seam /// The seam base can never fill: an injected ranker decides the winner ahead of the birth dates. @Test("An injected history ranker outranks the filesystem") func historyRankerDecides() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(original, date(0)) try setBirth(copy, date(1000)) // Without a ranker the older folder wins… #expect(try BoardLoader.load(boardRoot: fixture.root).duplicateIdentities.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"]) // …and with one that says the newer path entered history first, it does not. let ranker = BoardLoader.IdentityHistoryRanker { path in path == "\(Ident.lane2)/\(Dup.lower)" ? 1 : nil } #expect(try BoardLoader.load(boardRoot: fixture.root, historyRanker: ranker).duplicateIdentities.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"]) } /// Three copies of one card: two are withheld, one renders — and the notice folds. @Test("Three occurrences leave one standing") func threeCopiesLeaveOne() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.item(Ident.lane3, Item.rich(order: "3072", title: "Done")) let first = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let second = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let third = try fixture.item("\(Ident.lane3)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(first, date(0)) try setBirth(second, date(100)) try setBirth(third, date(200)) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.duplicateIdentities.map(\.path) == [ "\(Ident.lane2)/\(Dup.lower)", "\(Ident.lane3)/\(Dup.lower)", ]) #expect(renderedIdentities(result.model).filter { $0 == Dup.lower }.count == 1) } } // MARK: - The remint /// The Writer's half — a rename, and nothing else. @MainActor @Suite("Duplicate ids ▸ the remint") struct DuplicateIdentityRemintTests { private func board() throws -> (WriterFixture, DuplicateIdentity) { let fixture = try makeBoard() try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) return (fixture, DuplicateIdentity( path: "\(Ident.lane2)/\(Dup.lower)", identity: Dup.lower, title: "Fix login", winner: "\(Ident.lane1)/\(Dup.lower)" )) } /// **A fresh lowercase v4, content untouched, nothing stamped.** The remint is an identity repair, /// not an edit — the folder's `index.md` is never opened, so `modified` and `modified-by` are /// exactly what they were. @Test("It renames to a fresh lowercase v4 and leaves everything else alone") func remintsToAFreshName() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } let before = try fixture.indexText("\(Ident.lane2)/\(Dup.lower)") let minted = try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) let fresh = try #require(minted?.rawValue) #expect(fresh != Dup.lower) #expect(fresh == fresh.lowercased(), "the app emits lowercase") #expect(UUID(uuidString: fresh) != nil) #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)"), "the losing name is gone") #expect(try fixture.indexText("\(Ident.lane2)/\(fresh)") == before, "byte-verbatim, no stamp") // The winner never moved. #expect(fixture.exists("\(Ident.lane1)/\(Dup.lower)/index.md")) } /// Children travel with the folder — the rename never opens anything inside it. @Test("Attachments, strays and nested folders travel with the rename") func childrenTravel() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } try fixture.file("\(Ident.lane2)/\(Dup.lower)/attachments/shot.png", Data("png".utf8)) try fixture.file("\(Ident.lane2)/\(Dup.lower)/notes.txt", Data("loose".utf8)) let fresh = try #require(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue) #expect(try fixture.data("\(Ident.lane2)/\(fresh)/attachments/shot.png") == Data("png".utf8)) #expect(try fixture.data("\(Ident.lane2)/\(fresh)/notes.txt") == Data("loose".utf8)) } /// **The re-verify, first half** — the folder is gone. Losing the race to a foreign fix is /// success, never an error. @Test("A duplicate hand-deleted under the write is a no-op") func vanishedFolderIsANoOp() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } try FileManager.default.removeItem(at: fixture.url("\(Ident.lane2)/\(Dup.lower)")) #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil) } /// **The re-verify, second half** — the *winner* is gone, so this folder is no longer a duplicate /// of anything and reminting it would change an identity for no reason at all. @Test("A vanished winner makes the remint a no-op") func vanishedWinnerIsANoOp() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } try FileManager.default.removeItem(at: fixture.url("\(Ident.lane1)/\(Dup.lower)")) #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil) #expect(fixture.exists("\(Ident.lane2)/\(Dup.lower)/index.md"), "left exactly as it was") } /// Idempotent by the same re-verify: a second attempt on already-repaired work does nothing. @Test("Reminting twice remints once") func remintingTwiceRemintsOnce() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } let fresh = try #require(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue) #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil) #expect(fixture.exists("\(Ident.lane2)/\(fresh)/index.md")) } /// A case twin is never handed to this call by the loader — but if one were, the winner probe is /// what still makes it safe: the twin *is* a second occurrence, so the identity check is on the /// canonical value and the rename would be honest. What must not happen is a remint of a folder /// whose spelling no longer matches the defect that named it. @Test("A folder already reminted under us is not reminted again") func alreadyRemintedIsANoOp() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } try fixture.moveFolder("\(Ident.lane2)/\(Dup.lower)", to: "\(Ident.lane2)/\(Dup.other)") #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil) #expect(fixture.exists("\(Ident.lane2)/\(Dup.other)/index.md")) } /// A rename is a move as far as provenance goes, and **heal-marked**, because the app started it /// on its own — the receipt is what splits it into its own commit on git boards. @Test("It drops a heal-marked move receipt") func dropsAHealMarkedReceipt() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } let ledger = EchoLedger() let from = fixture.url("\(Ident.lane2)/\(Dup.lower)") let fresh = try EchoLedger.$current.withValue(ledger) { try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue } let to = fixture.url("\(Ident.lane2)/\(try #require(fresh))") #expect(ledger.receipt(at: from) == .move(from: EchoLedger.key(from), to: EchoLedger.key(to))) #expect(ledger.isHeal(at: to)) } /// The fresh name is minted away from **every** identity in the board, not merely from this /// parent's children: trading one duplicate for another would be the one unacceptable outcome. @Test("The minted name collides with nothing board-wide") func mintedNameIsBoardWideUnique() throws { let (fixture, duplicate) = try board() defer { fixture.tearDown() } try fixture.item("\(Ident.lane1)/\(Dup.other)", Item.rich(order: "2048", title: "Other")) try fixture.item(".trash/\(Ident.card3)", Item.rich(order: "1024", title: "Trashed")) let fresh = try #require(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.defects.isEmpty, "no duplicate left to find") #expect(renderedIdentities(result.model).contains(fresh)) #expect(Set(renderedIdentities(result.model)).count == renderedIdentities(result.model).count) } } // MARK: - The scheduled heal /// The store's half — the six-step engine, a notice, and no consent gate anywhere (re-ruled /// 2026-07-29: the user-gated Repair banner retired). @MainActor @Suite("Duplicate ids ▸ the scheduled heal") struct DuplicateIdentityHealTests { private func store() throws -> (WriterFixture, BoardStore) { let fixture = try makeBoard() let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(original, date(0)) try setBirth(copy, date(1000)) return (fixture, try BoardStore(rootURL: fixture.root)) } @Test("It remints the withheld occurrence and announces the repair") func remintsAndAnnounces() throws { let (fixture, store) = try store() defer { fixture.tearDown() } let brackets = BracketLog() brackets.attach(to: store) #expect(store.duplicateIdentities.count == 1) store.remintDuplicateIdentities() #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)")) #expect(store.banners.losses.map(\.message) == ["Repaired duplicate id — 'Fix login'"]) #expect(store.banners.oneShots.isEmpty, "nothing failed") #expect(brackets.begins == 1, "one bracket — one app-mediated reload, one commit") } /// The whole point of the re-ruling: the withheld window is **one heal cycle**, not a standing /// condition. After the heal, the reload renders both cards under distinct ids. @Test("The reload after the heal renders both cards and finds no defect") func theWindowClosesAfterOneCycle() async throws { let (fixture, store) = try store() defer { fixture.tearDown() } store.remintDuplicateIdentities() store.handleWatcherEvent(.treeChanged(.appMediated)) await store.awaitQuiescence() #expect(store.duplicateIdentities.isEmpty) #expect(store.snapshot.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) #expect(store.snapshot.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 1) #expect(Set(renderedIdentities(store.snapshot)).count == renderedIdentities(store.snapshot).count) } @Test("A reload fires it") func aReloadFiresIt() async throws { let fixture = try makeBoard() defer { fixture.tearDown() } let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let store = try BoardStore(rootURL: fixture.root) let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(original, date(0)) try setBirth(copy, date(1000)) store.handleWatcherEvent(.treeChanged(.foreign)) await store.awaitQuiescence() #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)")) #expect(store.banners.losses.count == 1) } /// **Defer, never abandon**: a read-only board writes nothing *and remembers nothing*, so the /// reload that lifts the lock is the reload that heals. @Test("A read-only board defers it") func lockDefersIt() throws { let (fixture, store) = try store() defer { fixture.tearDown() } store.enterUnwritableLock(.permissionDenied) store.remintDuplicateIdentities() #expect(fixture.exists("\(Ident.lane2)/\(Dup.lower)/index.md")) #expect(store.banners.losses.isEmpty) #expect(store.heals.memo(for: .duplicateIdentity) == nil, "deferred, not remembered") } /// **A repeat says nothing more**, and it is the disk re-verify rather than the memo that makes it /// so: the memo *clears on success* (that is load-bearing — see below), so a second call against a /// defect list the reload has not replaced yet does open a bracket. What it must not do is rename /// anything again or claim a second repair, and the Writer's re-verify is what guarantees both. @Test("A repeat attempt on already-repaired work says nothing more") func repeatingIsHarmless() throws { let (fixture, store) = try store() defer { fixture.tearDown() } store.remintDuplicateIdentities() let after = try fixture.entryNames(Ident.lane2) store.remintDuplicateIdentities() #expect(try fixture.entryNames(Ident.lane2) == after, "nothing renamed a second time") #expect(store.banners.losses.count == 1, "and nothing claimed twice") #expect(store.banners.oneShots.isEmpty) } /// Success clears the memo, which is what lets a *later* duplicate — the same picture restored by /// a foreign undo, a second hand copy — be healed again immediately. @Test("Success clears the memo") func successClearsTheMemo() throws { let (fixture, store) = try store() defer { fixture.tearDown() } store.remintDuplicateIdentities() #expect(store.heals.memo(for: .duplicateIdentity) == nil) } /// The no-op heal: the duplicate resolved itself between the load and the write. Nothing moves, /// nothing is said, nothing fails. @Test("A duplicate that vanished before the write is a silent no-op") func vanishedDuplicateIsASilentNoOp() throws { let (fixture, store) = try store() defer { fixture.tearDown() } #expect(store.duplicateIdentities.count == 1) try FileManager.default.removeItem(at: fixture.url("\(Ident.lane2)/\(Dup.lower)")) store.remintDuplicateIdentities() #expect(store.banners.losses.isEmpty, "nothing was claimed to have been repaired") #expect(store.banners.oneShots.isEmpty) } /// **Not undoable** (13-native-undo.md, re-ruled 2026-07-29): heals are not gestures, so nothing /// enters the stack — and undoing a remint would recreate the duplicate it exists to remove. @Test("It registers no undo step") func registersNoUndoStep() throws { let (fixture, store) = try store() defer { fixture.tearDown() } let steps = StepLog() store.history = steps store.remintDuplicateIdentities() #expect(steps.registered.isEmpty) } /// A hand-copied lane's worth of collisions arrive together and heal together, in one bracket, /// under one folded notice. @Test("Several duplicates heal in one bracket under one folded notice") func severalHealTogether() throws { let fixture = try makeBoard() defer { fixture.tearDown() } for (index, id) in [Dup.lower, Dup.other, Ident.card3].enumerated() { let original = try fixture.item("\(Ident.lane1)/\(id)", Item.rich(order: "\(1024 * (index + 1))", title: "Card \(index)")) let copy = try fixture.item("\(Ident.lane2)/\(id)", Item.rich(order: "\(1024 * (index + 1))", title: "Card \(index)")) try setBirth(original, date(0)) try setBirth(copy, date(1000)) } let store = try BoardStore(rootURL: fixture.root) let brackets = BracketLog() brackets.attach(to: store) #expect(store.duplicateIdentities.count == 3) store.remintDuplicateIdentities() #expect(brackets.begins == 1) #expect(store.banners.losses.map(\.message) == ["Repaired 3 duplicate ids"]) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.defects.isEmpty) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 3) } /// Case twins reach no heal at all: the store's defect stream never carries one, so the engine /// rests and the folders stay exactly where they are. @Test("A case twin schedules nothing") func caseTwinSchedulesNothing() throws { let fixture = try makeBoard() defer { fixture.tearDown() } try fixture.item("\(Ident.lane1)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login")) try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let store = try BoardStore(rootURL: fixture.root) let brackets = BracketLog() brackets.attach(to: store) store.remintDuplicateIdentities() #expect(brackets.begins == 0, "no work, no bracket") #expect(store.banners.losses.isEmpty) #expect(fixture.exists("\(Ident.lane1)/\(Dup.upper)/index.md")) } /// **The straddle healed end to end**: the ghost in the trash is the withheld occurrence, so it is /// the ghost that gets reminted — the restored card keeps the identity it was restored with, and /// the trash ends up holding an ordinary unrelated entry. @Test("The ghost is the one reminted, never the restored card") func theGhostIsReminted() throws { let fixture = try makeBoard() defer { fixture.tearDown() } let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login")) try setBirth(live, date(1000)) try setBirth(ghost, date(0)) let store = try BoardStore(rootURL: fixture.root) store.remintDuplicateIdentities() #expect(fixture.exists("\(Ident.lane1)/\(Dup.lower)/index.md"), "the restored card never moved") #expect(!fixture.exists(".trash/\(Dup.lower)")) #expect(store.banners.losses.map(\.message) == ["Repaired duplicate id — 'Fix login'"]) let result = try BoardLoader.load(boardRoot: fixture.root) #expect(result.defects.isEmpty) #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1) #expect(result.model.trash.count == 1) #expect(Set(renderedIdentities(result.model)).count == renderedIdentities(result.model).count) } /// It rides `runScheduledHeals()` like every other scheduled heal — the reload tail and board open. @Test("runScheduledHeals includes it") func runScheduledHealsIncludesIt() throws { let (fixture, store) = try store() defer { fixture.tearDown() } store.runScheduledHeals() #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)")) #expect(store.banners.losses.map(\.message) == ["Repaired duplicate id — 'Fix login'"]) } } // MARK: - Phrasing @Suite("Duplicate ids ▸ phrasing") struct DuplicateIdentityPhrasingTests { /// The design's own sentence, verbatim. @Test("One remint names the item") func oneRemintNamesTheItem() { #expect(BannerCenter.remintedDuplicateIDsMessage(for: ["Fix login"]) == "Repaired duplicate id — 'Fix login'") } @Test("An untitled item takes the untitled rendering") func untitled() { #expect(BannerCenter.remintedDuplicateIDsMessage(for: [nil]) == "Repaired duplicate id — an untitled item") } @Test("Several fold to a count") func severalFold() { #expect(BannerCenter.remintedDuplicateIDsMessage(for: ["Fix login", nil, "Ship it"]) == "Repaired 3 duplicate ids") } @Test("Nothing reminted says nothing") func nothingSaysNothing() { #expect(BannerCenter.remintedDuplicateIDsMessage(for: []) == nil) } /// It rides the **loss-row** class: warning tone, user-dismissed, never expiring — the /// relocation's class, because it is the same kind of event. Emphatically not a condition banner /// with a button: that is the surface the 2026-07-29 re-ruling retired. @Test("It rides the loss-row class, with no condition and no button") @MainActor func ridesTheLossClass() { let banners = BannerCenter() banners.postRemintedDuplicateIDs(["Fix login"]) #expect(banners.losses.count == 1) #expect(banners.oneShots.isEmpty) #expect(banners.signposts.isEmpty) } /// The failure's mirror, in the one-shot vocabulary the Writer's errors reach the strip through. @Test("A failed remint says so") func failureSaysSo() { let error = BoardWriteError( operation: .repairDuplicateID(title: "Fix login"), path: "/b/lane/card", reason: .io(message: "permission denied") ) #expect(BannerCenter.headline(for: error) == "Couldn't repair the duplicate id of 'Fix login' — permission denied") } @Test("An untitled failure stays graceful") func untitledFailure() { let error = BoardWriteError( operation: .repairDuplicateID(title: nil), path: "/b/lane/card", reason: .io(message: "disk full") ) #expect(BannerCenter.headline(for: error) == "Couldn't repair a duplicate id — disk full") } /// The Repair verb survives the re-ruling as the operation's own word — 06-history-undo.md keeps /// it as the heal commit's name. @Test("The operation describes itself with the Repair verb") func operationDescribesItself() { #expect(WriteOperation.repairDuplicateID(title: "Fix login").description == "repair the duplicate id of 'Fix login'") } /// The remint never opens an `index.md`, so there is no document to enrich its title from — it /// arrives already filled in from the load. @Test("withTitle leaves it alone") func withTitleIsIdentity() { #expect(WriteOperation.repairDuplicateID(title: "Fix login").withTitle("Something else") == .repairDuplicateID(title: "Fix login")) } }