import Foundation import SwiftData import Testing import IndieSync @testable import Workouts /// Locks the bundled starter-routine library end to end: the catalog decodes and /// orders correctly, the pinned seed ids never drift, a pristine save re-emits /// byte-identical JSON (no phantom iCloud churn), every seed passes the /// forward-compatibility gate, and `isPristine` tracks edits correctly through /// both the raw document and a SwiftData cache round-trip. struct SeedLibraryTests { @Test func catalogLoadsFourteenSeedsInOrder() { let seeds = SeedLibrary.seeds #expect(seeds.count == 14) #expect(seeds.map(\.doc.order) == Array(0...13)) #expect(seeds.map(\.doc.name) == [ "Upper Body", "Core", "Lower Body", "Bodyweight Core", "Full Body Machines", "Free Weight Basics", "Upper Body Warm-Up", "Lower Body Warm-Up", "Morning Wake-Up", "Morning Mobility", "Full Body Stretch", "Evening Stretch", "Cardio", "Meditation", ]) } /// These ids are minted from a frozen 2020 timestamp and are already shipped /// inside installed app bundles. Changing any of them breaks cross-device /// convergence (two devices would mint different ids for "the same" starter /// routine) and voids the permanent-tombstone exemption that lets an /// edited-then-deleted seed's stub veto resurrection forever — regenerate /// only with extreme care, and never for a seed that has already shipped. @Test func seedIDsArePinned() { #expect(SeedLibrary.seedIDs == [ "01DXF6DT0038BDC2WC3EVX8ZJ5", // Upper Body "01DXF6DT001MA0TM7FHJZT098Z", // Core "01DXF6DT006QRF1PMGK17FV505", // Lower Body "01DXF6DT00RV99WG172YFW4NKA", // Bodyweight Core "01DXF6DT00SZ0NPXPN36415NKM", // Full Body Machines "01DXF6DT00WY3HMCH6E841YQE7", // Free Weight Basics "01DXF6DT00G56WC0HS6RJWMFMD", // Upper Body Warm-Up "01DXF6DT003QB4ASSR8JB1JT6A", // Lower Body Warm-Up "01DXF6DT00FJ5F1HRENQPC1DTG", // Morning Wake-Up "01DXF6DT00QHJ9G4SDMEX64NZD", // Morning Mobility "01DXF6DT003PRJB3FGYSSGTQ5X", // Full Body Stretch "01DXF6DT00M5YRFPHJQ2FS2B9P", // Evening Stretch "01DXF6DT00BXJ6N63ZBX9M9VNH", // Cardio "01DXF6DT00ANFJR7GX9G808G4M", // Meditation ]) } /// A pristine save must re-emit the exact bundle bytes, or iCloud would see a /// modification on every install/upgrade that touches a never-edited seed. @Test func encodingReproducesBundleBytesExactly() throws { for seed in SeedLibrary.seeds { #expect(try DocumentCoder.encode(seed.doc) == seed.data) } } @Test func allSeedsAreReadableAtCurrentSchemaVersion() { for seed in SeedLibrary.seeds { #expect(seed.doc.isReadable) #expect(seed.doc.schemaVersion == RoutineDocument.currentSchemaVersion) } } @Test func pristineSeedsAreThemselvesPristine() { for seed in SeedLibrary.seeds { #expect(SeedLibrary.isPristine(seed.doc)) } } /// Edit sheets stamp `updatedAt` on every Save, so a no-op save must not read /// as an edit — otherwise saving without changing anything would fork the seed. @Test func pristineIgnoresUpdatedAtOnlyChange() throws { let seed = try #require(SeedLibrary.seeds.first) var edited = seed.doc edited.updatedAt = Date() #expect(SeedLibrary.isPristine(edited)) } @Test func pristineDetectsExerciseWeightChange() throws { let seed = try #require(SeedLibrary.seeds.first) var edited = seed.doc edited.exercises[0].weight += 5 #expect(!SeedLibrary.isPristine(edited)) } @Test func pristineDetectsNameChange() throws { let seed = try #require(SeedLibrary.seeds.first) var edited = seed.doc edited.name = "Renamed" #expect(!SeedLibrary.isPristine(edited)) } /// Dragging a starter to reorder the Library list writes a new `order` to it. Order /// is presentation, not content, so a reorder must NOT fork the seed — an order-only /// change reads as pristine. @Test func pristineIgnoresOrderOnlyChange() throws { let seed = try #require(SeedLibrary.seeds.first) var edited = seed.doc edited.order += 5 #expect(SeedLibrary.isPristine(edited)) } /// A reorder save also stamps `updatedAt`; order + updatedAt together must still read /// as pristine so a drag never forks a starter. @Test func pristineIgnoresOrderAndUpdatedAtChange() throws { let seed = try #require(SeedLibrary.seeds.first) var edited = seed.doc edited.order += 3 edited.updatedAt = Date() #expect(SeedLibrary.isPristine(edited)) } /// Order tolerance must not mask a genuine content edit: a real change (here, a /// different rest length) is still detected even when `order` also differs. @Test func pristineDetectsContentChangeDespiteOrderChange() throws { let seed = try #require(SeedLibrary.seeds.first) var edited = seed.doc edited.order += 2 edited.restSeconds = (seed.doc.restSeconds ?? 60) + 30 #expect(!SeedLibrary.isPristine(edited)) } /// A non-seed id has no seed to compare against; `isPristine` defaults to /// true since callers are expected to gate on `isSeed` before asking. @Test func pristineDefaultsTrueForNonSeedID() throws { let seed = try #require(SeedLibrary.seeds.first) var notASeed = seed.doc notASeed.id = ULID.make() #expect(SeedLibrary.isPristine(notASeed)) } /// Round-trips every seed through the SwiftData cache the way the metadata /// observer would (document → `CacheMapper.upsertRoutine` → entity), then /// rebuilds a document from that entity the way the write path would. The /// result must still read as pristine, or every user's very first cache /// rebuild would silently fork their starter routines. @MainActor @Test func mapperRoundTripPreservesPristineness() throws { let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self]) // cloudKitDatabase: .none matches WorkoutsModelContainer — without it SwiftData // applies CloudKit schema validation (all relationships optional), which the // cache entities intentionally don't satisfy. let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none) let container = try ModelContainer(for: schema, configurations: [config]) let context = ModelContext(container) for seed in SeedLibrary.seeds { CacheMapper.upsertRoutine(seed.doc, relativePath: seed.doc.relativePath, into: context) let entity = try #require(CacheMapper.fetchRoutine(id: seed.id, in: context)) let rebuilt = RoutineDocument(from: entity) #expect(SeedLibrary.isPristine(rebuilt)) } } }