import Foundation import SwiftData import Testing import IndieSync @testable import Workouts /// Locks the bundled starter-split 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 catalogLoadsFourSeedsInOrder() { let seeds = SeedLibrary.seeds #expect(seeds.count == 4) #expect(seeds.map(\.doc.order) == [0, 1, 2, 3]) #expect(seeds.map(\.doc.name) == ["Upper Body", "Core", "Lower Body", "Bodyweight Core"]) } /// 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 /// split) 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 ]) } /// 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 == SplitDocument.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)) } /// 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.upsertSplit` → 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 splits. @MainActor @Test func mapperRoundTripPreservesPristineness() throws { let schema = Schema([Split.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.upsertSplit(seed.doc, relativePath: seed.doc.relativePath, into: context) let entity = try #require(CacheMapper.fetchSplit(id: seed.id, in: context)) let rebuilt = SplitDocument(from: entity) #expect(SeedLibrary.isPristine(rebuilt)) } } }