Treat order as presentation in seed logic; add per-seed restore and routine duplication
Drag-to-reorder writes Routine.order through the document, which used to read as a content edit: isPristine would fork a starter for a mere reorder, and reconcile's semantic compare would clobber a reordered seed file back to bundle order. Both now normalize order (and updatedAt) away — a fixed- ULID file still never holds user content; ordering is bookkeeping. SyncEngine gains restoreSeed(id:) — the per-seed analog of the bulk restore, sharing one restoreSeedIfEligible core — and duplicate(routine:), which copies any routine (starter or not) to a fresh ULID with fresh exercise ids, a unique "… Copy" name, and last position in the list. Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
This commit is contained in:
@@ -85,6 +85,44 @@ struct SeedReconcilePlannerTests {
|
||||
#expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .skip(.quarantined))
|
||||
}
|
||||
|
||||
// MARK: - Order tolerance (user reordered their starters)
|
||||
|
||||
/// The user dragged this starter to a new position, writing a new `order` to its
|
||||
/// fixed-ULID file. That is presentation, not content, so reconcile must read it as
|
||||
/// up to date and never clobber the ordering back to the bundle's default.
|
||||
@Test func orderOnlyDriftIsUpToDate() {
|
||||
let bundle = seed() // order 0
|
||||
var live = bundle
|
||||
live.order = 7 // user reordered their starters
|
||||
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
|
||||
|
||||
#expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .skip(.upToDate))
|
||||
}
|
||||
|
||||
/// A reorder save also stamps `updatedAt`; order + updatedAt drift together must
|
||||
/// still read as up to date.
|
||||
@Test func orderAndUpdatedAtDriftIsUpToDate() {
|
||||
let bundle = seed()
|
||||
var live = bundle
|
||||
live.order = 4
|
||||
live.updatedAt = Date() // a reorder save bumps updatedAt too
|
||||
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
|
||||
|
||||
#expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .skip(.upToDate))
|
||||
}
|
||||
|
||||
/// Order tolerance must not mask a genuine content upgrade: when the bundle content
|
||||
/// changed AND the user had reordered (order differs too), it still upgrades — the
|
||||
/// order normalization can't swallow the real content diff.
|
||||
@Test func contentUpgradeWinsOverOrderTolerance() {
|
||||
let bundle = seed(exercises: [exercise(name: "Bench Press"), exercise(name: "Overhead Press", order: 1)])
|
||||
var live = seed(exercises: [exercise(name: "Bench Press")]) // bundle gained an exercise
|
||||
live.order = 9 // and the user had reordered
|
||||
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
|
||||
|
||||
#expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .upgrade)
|
||||
}
|
||||
|
||||
// MARK: - No live file
|
||||
|
||||
/// No live file but a stub exists → the user deleted this seed. The veto stands.
|
||||
|
||||
Reference in New Issue
Block a user