polscm32 1 anno fa
parent
commit
bdc4409954

+ 52 - 0
Trio.xcodeproj/project.pbxproj

@@ -317,6 +317,15 @@
 		BD8207C42D2B42E60023339D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		BD8207C52D2B42E60023339D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
 		BD8207CE2D2B42E70023339D /* Trio Watch Complication Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		BD8FC0542D66186000B95AED /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0532D66186000B95AED /* TestError.swift */; };
+		BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */; };
+		BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0582D66189700B95AED /* TestAssembly.swift */; };
+		BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */; };
+		BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */; };
+		BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */; };
+		BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */; };
+		BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */; };
+		BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */; };
 		BDA25EE42D260CD500035F34 /* AppleWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */; };
 		BDA25EE62D260D5E00035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE52D260D5800035F34 /* WatchState.swift */; };
 		BDA25EFD2D261C0000035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EFC2D261BF200035F34 /* WatchState.swift */; };
@@ -1043,6 +1052,15 @@
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculationManager.swift; sourceTree = "<group>"; };
 		BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Trio Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+		BD8FC0532D66186000B95AED /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = "<group>"; };
+		BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0582D66189700B95AED /* TestAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssembly.swift; sourceTree = "<group>"; };
+		BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorTests.swift; sourceTree = "<group>"; };
+		BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorageTests.swift; sourceTree = "<group>"; };
 		BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchManager.swift; sourceTree = "<group>"; };
 		BDA25EE52D260D5800035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
 		BDA25EFC2D261BF200035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
@@ -2296,10 +2314,13 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			children = (
+				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
+				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
+				BD8FC0532D66186000B95AED /* TestError.swift */,
 			);
 			path = TrioTests;
 			sourceTree = "<group>";
@@ -2543,6 +2564,28 @@
 			path = BolusCalculator;
 			sourceTree = "<group>";
 		};
+		BD8FC0552D66187700B95AED /* CoreDataTests */ = {
+			isa = PBXGroup;
+			children = (
+				BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */,
+				BD8FC0582D66189700B95AED /* TestAssembly.swift */,
+				BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */,
+				BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */,
+				BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */,
+				BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */,
+				BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */,
+			);
+			path = CoreDataTests;
+			sourceTree = "<group>";
+		};
+		BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */ = {
+			isa = PBXGroup;
+			children = (
+				BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */,
+			);
+			path = BolusCalculatorTests;
+			sourceTree = "<group>";
+		};
 		BDA25F1A2D26BCE800035F34 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -4060,9 +4103,18 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */,
+				BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */,
+				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
+				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
+				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
+				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
+				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
+				BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */,
+				BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 54 - 0
Trio.xcodeproj/xcshareddata/xcschemes/Trio Tests.xcscheme

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1620"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "38FCF3EC25E9028E0078B0D1"
+               BuildableName = "TrioTests.xctest"
+               BlueprintName = "TrioTests"
+               ReferencedContainer = "container:Trio.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 19 - 18
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -26,15 +26,16 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settings: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.newTaskContext()
-
     private let updateSubject = PassthroughSubject<Void, Never>()
 
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
     }
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
@@ -75,7 +76,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         // Fetch only the date property from Core Data
         guard let existing24hCarbEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             key: "date",
             ascending: false,
@@ -217,8 +218,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
         guard let entry = entries.last else { return }
 
-        await coredataContext.perform {
-            let newItem = CarbEntryStored(context: self.coredataContext)
+        await context.perform {
+            let newItem = CarbEntryStored(context: self.context)
             newItem.date = entry.actualDate ?? entry.createdAt
             newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
             newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
@@ -235,8 +236,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             }
 
             do {
-                guard self.coredataContext.hasChanges else { return }
-                try self.coredataContext.save()
+                guard self.context.hasChanges else { return }
+                try self.context.save()
             } catch {
                 print(error.localizedDescription)
             }
@@ -264,9 +265,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             // do NOT set Health and Tidepool flags to ensure they will NOT be uploaded
             return false // return false to continue
         }
-        await coredataContext.perform {
+        await context.perform {
             do {
-                try self.coredataContext.execute(batchInsert)
+                try self.context.execute(batchInsert)
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
 
                 // Notify subscriber in Home State Model to update the FPU Array
@@ -342,13 +343,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -381,13 +382,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fpuEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -420,13 +421,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -451,13 +452,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getCarbsNotYetUploadedToTidepool() async throws -> [CarbsEntry] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

+ 3 - 2
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -18,9 +18,10 @@ protocol DeterminationStorage {
 
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let context = CoreDataStack.shared.newTaskContext()
+    private let context: NSManagedObjectContext
 
-    init(resolver: Resolver) {
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 

+ 30 - 29
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -33,8 +33,6 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settingsManager: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.newTaskContext()
-
     private let updateSubject = PassthroughSubject<Void, Never>()
 
     var updatePublisher: AnyPublisher<Void, Never> {
@@ -45,7 +43,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         static let filterTime: TimeInterval = 3.5 * 60
     }
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
@@ -61,7 +62,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     func storeGlucose(_ glucose: [BloodGlucose]) async throws {
-        try await coredataContext.perform {
+        try await context.perform {
             // Get new glucose values that don't exist yet
             let newGlucose = self.filterNewGlucoseValues(glucose)
             guard !newGlucose.isEmpty else { return }
@@ -93,7 +94,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
         var existingDates = Set<Date>()
         do {
-            let results = try coredataContext.fetch(fetchRequest) as? [NSDictionary]
+            let results = try context.fetch(fetchRequest) as? [NSDictionary]
             existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
         } catch {
             debugPrint("Failed to fetch existing glucose dates: \(error)")
@@ -112,12 +113,12 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     private func storeGlucoseRegular(_ glucose: [BloodGlucose]) throws {
         for entry in glucose {
-            let glucoseEntry = GlucoseStored(context: coredataContext)
+            let glucoseEntry = GlucoseStored(context: context)
             configureGlucoseEntry(glucoseEntry, with: entry)
         }
 
-        guard coredataContext.hasChanges else { return }
-        try coredataContext.save()
+        guard context.hasChanges else { return }
+        try context.save()
     }
 
     private func storeGlucoseBatch(_ glucose: [BloodGlucose]) throws {
@@ -135,7 +136,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 return false
             }
         )
-        try coredataContext.execute(batchInsert)
+        try context.execute(batchInsert)
         // Only send update for batch insert since regular save triggers CoreData notifications
         updateSubject.send()
     }
@@ -218,8 +219,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     func addManualGlucose(glucose: Int) {
-        coredataContext.perform {
-            let newItem = GlucoseStored(context: self.coredataContext)
+        context.perform {
+            let newItem = GlucoseStored(context: self.context)
             newItem.id = UUID()
             newItem.date = Date()
             newItem.glucose = Int16(glucose)
@@ -229,8 +230,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             newItem.isUploadedToTidepool = false
 
             do {
-                guard self.coredataContext.hasChanges else { return }
-                try self.coredataContext.save()
+                guard self.context.hasChanges else { return }
+                try self.context.save()
 
                 // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
                 self.updateSubject.send()
@@ -281,9 +282,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         fr.fetchLimit = 1
 
         var date: Date?
-        coredataContext.performAndWait {
+        context.performAndWait {
             do {
-                let results = try self.coredataContext.fetch(fr)
+                let results = try self.context.fetch(fr)
                 date = results.first?.date
             } catch let error as NSError {
                 print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
@@ -317,7 +318,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         let predicate = NSPredicate.predicateFor20MinAgo
         return (try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: predicate,
             key: "date",
             ascending: false,
@@ -330,13 +331,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -363,13 +364,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -419,13 +420,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -451,13 +452,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -483,13 +484,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -516,13 +517,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -572,7 +573,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
-        coredataContext.performAndWait {
+        context.performAndWait {
             do {
                 guard let glucose = try fetchLatestGlucose() else { return nil }
 

+ 21 - 20
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -20,9 +20,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     @Injected() private var settingsManager: SettingsManager!
 
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let context: NSManagedObjectContext
 
-    init(resolver: Resolver) {
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
@@ -37,7 +38,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func fetchLastCreatedOverride() async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(
                 format: "date >= %@",
                 Date.oneDayAgo as NSDate
@@ -47,7 +48,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -59,14 +60,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func loadLatestOverrideConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveOverride,
             key: "orderPosition",
             ascending: true,
             fetchLimit: fetchLimit
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -79,13 +80,13 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func fetchForOverridePresets() async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.allOverridePresets,
             key: "orderPosition",
             ascending: true
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -108,8 +109,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             presetCount = presets.count
         }
 
-        try await backgroundContext.perform {
-            let newOverride = OverrideStored(context: self.backgroundContext)
+        try await context.perform {
+            let newOverride = OverrideStored(context: self.context)
 
             // override key meta data
             if !override.name.isEmpty {
@@ -157,8 +158,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                 newOverride.smbIsScheduledOff = false
             }
 
-            guard self.backgroundContext.hasChanges else { return }
-            try self.backgroundContext.save()
+            guard self.context.hasChanges else { return }
+            try self.context.save()
         }
     }
 
@@ -209,13 +210,13 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedOverrides = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -237,7 +238,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(
                 format: "startDate >= %@ AND isUploadedToNS == %@",
                 Date.oneDayAgo as NSDate,
@@ -247,7 +248,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedOverrideRuns = results as? [OverrideRunStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -270,13 +271,13 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.allOverridePresets,
             key: "orderPosition",
             ascending: true
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -299,14 +300,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func fetchLatestActiveOverride() async throws -> NSManagedObjectID? {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveOverride,
             key: "date",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored],
                   let latestOverride = fetchedResults.first
             else {

+ 4 - 2
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -25,13 +25,15 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var settings: SettingsManager!
 
     private let updateSubject = PassthroughSubject<Void, Never>()
-    private let context = CoreDataStack.shared.newTaskContext()
 
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
     }
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 

+ 22 - 20
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -31,24 +31,26 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settingsManager: SettingsManager!
 
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
     func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveTempTarget,
             key: "orderPosition",
             ascending: true,
             fetchLimit: fetchLimit
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -61,13 +63,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     func fetchForTempTargetPresets() async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.allTempTargetPresets,
             key: "orderPosition",
             ascending: true
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -81,13 +83,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
 
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: scheduledTempTargets,
             key: "date",
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -101,14 +103,14 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
 
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: predicate,
             key: "date",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -124,8 +126,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             presetCount = presets.count
         }
 
-        try await backgroundContext.perform {
-            let newTempTarget = TempTargetStored(context: self.backgroundContext)
+        try await context.perform {
+            let newTempTarget = TempTargetStored(context: self.context)
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.id = UUID()
             newTempTarget.enabled = tempTarget.enabled ?? false
@@ -149,8 +151,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             }
 
             do {
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard self.context.hasChanges else { return }
+                try self.context.save()
             } catch let error as NSError {
                 debug(.default, "\(DebuggingIdentifiers.failed) Failed to save new temp target with error: \(error.userInfo)")
                 throw error
@@ -180,13 +182,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     }
 
     func existsTempTarget(with date: Date) async -> Bool {
-        await backgroundContext.perform {
+        await context.perform {
             // Fetch all Temp Targets with the given date
             let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "date == %@", date as NSDate)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try self.context.fetch(fetchRequest)
                 return !results.isEmpty
             } catch let error as NSError {
                 debugPrint("\(DebuggingIdentifiers.failed) Failed to check for existing Temp Target: \(error)")
@@ -252,13 +254,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     func getTempTargetsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedTempTargets = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -289,7 +291,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     func getTempTargetRunsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(
                 format: "startDate >= %@ AND isUploadedToNS == %@",
                 Date.oneDayAgo as NSDate,
@@ -299,7 +301,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

+ 590 - 0
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

@@ -0,0 +1,590 @@
+import Foundation
+import Testing
+
+@testable import Trio
+
+@Suite("Bolus Calculator Tests") struct BolusCalculatorTests: Injectable {
+    @Injected() var calculator: BolusCalculationManager!
+    @Injected() var settingsManager: SettingsManager!
+    @Injected() var fileStorage: FileStorage!
+    @Injected() var apsManager: APSManager!
+    let resolver = TrioApp().resolver
+
+    init() {
+        injectServices(resolver)
+    }
+
+    @Test("Calculator is correctly initialized") func testCalculatorInitialization() {
+        #expect(calculator != nil, "BolusCalculationManager should be injected")
+        #expect(calculator is BaseBolusCalculationManager, "Calculator should be of type BaseBolusCalculationManager")
+    }
+
+    @Test("Calculate insulin for standard meal") func testStandardMealCalculation() async throws {
+        // STEP 1: Setup test scenario
+        // We need to provide a CalculationInput struct
+        let carbs: Decimal = 80
+        let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
+        let deltaBG: Decimal = 5 // Rising trend, should add small correction
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10 // Should result in 8U for carbs
+        let iob: Decimal = 1.0 // Should subtract from final result
+        let cob: Int16 = 20
+        let useFattyMealCorrectionFactor: Bool = false
+        let useSuperBolus: Bool = false
+        let fattyMealFactor: Decimal = 0.8
+        let sweetMealFactor: Decimal = 2
+        let basal: Decimal = 1.5
+        let fraction: Decimal = 0.8
+        let maxBolus: Decimal = 10
+
+        // STEP 2: Create calculation input
+        let input = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+
+        // STEP 3: Calculate insulin
+        let result = await calculator.calculateInsulin(input: input)
+
+        // STEP 4: Verify results
+        // Expected calculation breakdown:
+        // wholeCob = 80g + 20g COB = 100g
+        // wholeCobInsulin = 100g ÷ 10 g/U = 10U
+        // targetDifference = currentBG - target = 180 - 100 = 80 mg/dL
+        // targetDifferenceInsulin = 80 mg/dL ÷ 40 mg/dL/U = 2U
+        // fifteenMinutesInsulin = 5 mg/dL ÷ 40 mg/dL/U = 0.125U
+        // correctionInsulin = targetDifferenceInsulin = 2U
+        // iobInsulinReduction = 1U
+        // superBolusInsulin = 0U (disabled)
+        // no adjustment for fatty meals (disabled)
+        // wholeCalc = round(wholeCobInsulin + correctionInsulin + fifteenMinutesInsulin - iobInsulinReduction, 3) = 11.125U
+        // insulinCalculated = round(wholeCalc × fraction, 3) = 8.9U
+
+        // Calculate expected values with proper rounding using roundBolus method from the apsManager
+        let wholeCobInsulin = apsManager.roundBolus(amount: Decimal(100) / Decimal(10)) // 10U
+        let targetDifferenceInsulin = apsManager.roundBolus(amount: Decimal(80) / Decimal(40)) // 2U
+        let fifteenMinutesInsulin = apsManager.roundBolus(amount: Decimal(5) / Decimal(40)) // 0.125U
+        let wholeCalc = wholeCobInsulin + targetDifferenceInsulin + fifteenMinutesInsulin - Decimal(1) // 11.125U
+        let expectedInsulinCalculated = apsManager.roundBolus(amount: wholeCalc * fraction) // 8.9U
+
+        #expect(
+            result.insulinCalculated == expectedInsulinCalculated,
+            """
+            Incorrect insulin calculation
+            Expected: \(expectedInsulinCalculated)U
+            Actual: \(result.insulinCalculated)U
+            Components from CalculationResult:
+            - insulinCalculated: \(result.insulinCalculated)U (expected: \(expectedInsulinCalculated)U)
+            - wholeCalc: \(result.wholeCalc)U (expected: \(wholeCalc)U)
+            - correctionInsulin: \(result.correctionInsulin)U (expected: \(targetDifferenceInsulin)U)
+            - iobInsulinReduction: \(result.iobInsulinReduction)U (expected: 1U)
+            - superBolusInsulin: \(result.superBolusInsulin)U (expected: 0U)
+            - targetDifference: \(result.targetDifference) mg/dL (expected: 80 mg/dL)
+            - targetDifferenceInsulin: \(result.targetDifferenceInsulin)U (expected: \(targetDifferenceInsulin)U)
+            - fifteenMinutesInsulin: \(result.fifteenMinutesInsulin)U (expected: \(fifteenMinutesInsulin)U)
+            - wholeCob: \(result.wholeCob)g (expected: 100g)
+            - wholeCobInsulin: \(result.wholeCobInsulin)U (expected: \(wholeCobInsulin)U)
+            """
+        )
+
+        // Verify each component from CalculationResult struct with rounded values
+        #expect(
+            result.insulinCalculated == expectedInsulinCalculated,
+            "Final calculated insulin amount should be \(expectedInsulinCalculated)U"
+        )
+        #expect(result.wholeCalc == wholeCalc, "Total calculation before fraction should be \(wholeCalc)U")
+        #expect(
+            result.correctionInsulin == targetDifferenceInsulin,
+            "Insulin for BG correction should be \(targetDifferenceInsulin)U"
+        )
+        #expect(result.iobInsulinReduction == -1.0, "Absolute IOB reduction amount should be 1U, hence -1U")
+        #expect(result.superBolusInsulin == 0, "Additional insulin for super bolus should be 0U")
+        #expect(result.targetDifference == 80, "Difference from target BG should be 80 mg/dL")
+        #expect(
+            result.targetDifferenceInsulin == targetDifferenceInsulin,
+            "Insulin needed for target difference should be \(targetDifferenceInsulin)U"
+        )
+        #expect(
+            result.fifteenMinutesInsulin == fifteenMinutesInsulin,
+            "Trend-based insulin adjustment should be \(fifteenMinutesInsulin)U"
+        )
+        #expect(result.wholeCob == 100, "Total carbs (COB + new carbs) should be 100g")
+        #expect(result.wholeCobInsulin == wholeCobInsulin, "Insulin for total carbs should be \(wholeCobInsulin)U")
+    }
+
+    @Test("Calculate insulin for fatty meal") func testFattyMealCalculation() async throws {
+        // STEP 1: Setup test scenario
+        // We need to provide a CalculationInput struct
+        let carbs: Decimal = 80
+        let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
+        let deltaBG: Decimal = 5 // Rising trend, should add small correction
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10 // Should result in 8U for carbs
+        let iob: Decimal = 1.0 // Should subtract from final result
+        let cob: Int16 = 20
+        let useFattyMealCorrectionFactor: Bool = true // now set to true
+        let useSuperBolus: Bool = false
+        let fattyMealFactor: Decimal = 0.8
+        let sweetMealFactor: Decimal = 2
+        let basal: Decimal = 1.5
+        let fraction: Decimal = 0.8
+        let maxBolus: Decimal = 10
+
+        // STEP 2: Create calculation input
+        let input = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+
+        // STEP 3: Calculate insulin with fatty meal enabled
+        let fattyMealResult = await calculator.calculateInsulin(input: input)
+
+        // STEP 4: Calculate insulin with fatty meal disabled for comparison
+        let standardInput = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: false, // Disabled for comparison
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+        let standardResult = await calculator.calculateInsulin(input: standardInput)
+
+        // STEP 5: Verify results
+        // Fatty meal should reduce the insulin amount by the fatty meal factor (0.8)
+        let expectedReduction = fattyMealFactor
+        let actualReduction = Decimal(
+            (Double(fattyMealResult.insulinCalculated) / Double(standardResult.insulinCalculated) * 10.0).rounded() / 10.0
+        )
+
+        #expect(
+            actualReduction == expectedReduction,
+            """
+            Fatty meal calculation incorrect
+            Expected reduction factor: \(expectedReduction)
+            Actual reduction factor: \(actualReduction)
+            Standard calculation: \(standardResult.insulinCalculated)U
+            Fatty meal calculation: \(fattyMealResult.insulinCalculated)U
+            """
+        )
+    }
+
+    @Test("Calculate insulin with super bolus") func testSuperBolusCalculation() async throws {
+        // STEP 1: Setup test scenario
+        // We need to provide a CalculationInput struct
+        let carbs: Decimal = 80
+        let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
+        let deltaBG: Decimal = 5 // Rising trend, should add small correction
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10 // Should result in 8U for carbs
+        let iob: Decimal = 1.0 // Should subtract from final result
+        let cob: Int16 = 20
+        let useFattyMealCorrectionFactor: Bool = false
+        let useSuperBolus: Bool = true // Super bolus enabled
+        let fattyMealFactor: Decimal = 0.8
+        let sweetMealFactor: Decimal = 2
+        let basal: Decimal = 1.5 // Will be added to insulin calculation when super bolus is enabled
+        let fraction: Decimal = 0.8
+        let maxBolus: Decimal = 10
+
+        // STEP 2: Create calculation input with super bolus enabled
+        let input = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+
+        // STEP 3: Calculate insulin with super bolus enabled
+        let superBolusResult = await calculator.calculateInsulin(input: input)
+
+        // STEP 4: Calculate insulin with super bolus disabled for comparison
+        let standardInput = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: false, // Disabled for comparison
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+        let standardResult = await calculator.calculateInsulin(input: standardInput)
+
+        // STEP 5: Verify results
+        // Super bolus should add basal rate * sweetMealFactor to the insulin calculation
+        let expectedSuperBolusInsulin = basal * sweetMealFactor
+        #expect(
+            superBolusResult.superBolusInsulin == expectedSuperBolusInsulin,
+            """
+            Super bolus insulin incorrect
+            Expected: \(expectedSuperBolusInsulin)U (basal \(basal)U × sweetMealFactor \(sweetMealFactor))
+            Actual: \(superBolusResult.superBolusInsulin)U
+            """
+        )
+
+        #expect(
+            superBolusResult.insulinCalculated > standardResult.insulinCalculated,
+            """
+            Super bolus calculation incorrect
+            Expected super bolus calculation to be higher than standard
+            Super bolus: \(superBolusResult.insulinCalculated)U
+            Standard: \(standardResult.insulinCalculated)U
+            Difference: \(superBolusResult.insulinCalculated - standardResult.insulinCalculated)U
+            """
+        )
+
+        // The difference should be the difference of super bolus (= standard dose + the basal rate * sweetMealFactor) limited by max bolus, and the standard dose.
+        let actualDifference = (superBolusResult.insulinCalculated - standardResult.insulinCalculated)
+        let expectedDifference = min(superBolusResult.insulinCalculated, maxBolus) - standardResult.insulinCalculated
+        #expect(
+            actualDifference == expectedDifference,
+            """
+            Super bolus difference incorrect
+            Expected difference: min(\(expectedSuperBolusInsulin), \(maxBolus)) U (basal \(basal)U × sweetMealFactor \(sweetMealFactor) + standard dose \(standardResult
+                .insulinCalculated)) - standard dose \(standardResult.insulinCalculated)
+            Actual difference: \(actualDifference)U
+            Standard result: \(standardResult)
+            SuperBolus result: \(superBolusResult)
+            """
+        )
+    }
+
+    @Test("Calculate insulin with zero carbs") func testZeroCarbsCalculation() async throws {
+        // Given
+        let carbs: Decimal = 0
+
+        // When
+        let result = await calculator.handleBolusCalculation(
+            carbs: carbs,
+            useFattyMealCorrection: false,
+            useSuperBolus: false
+        )
+
+        // Then
+        #expect(result.wholeCobInsulin == 0, "Zero carbs should require no insulin for carbs")
+    }
+
+    @Test("Verify settings retrieval") func testGetSettings() async throws {
+        // Given - Save original settings to restore later
+        let originalSettings = settingsManager.settings
+
+        // Setup test settings
+        let expectedUnits = GlucoseUnits.mgdL
+        let expectedFraction: Decimal = 0.7
+        let expectedFattyMealFactor: Decimal = 0.8
+        let expectedSweetMealFactor: Decimal = 2
+        let expectedMaxCarbs: Decimal = 150
+
+        // Update settings through settings manager
+        settingsManager.settings.units = expectedUnits
+        settingsManager.settings.overrideFactor = expectedFraction
+        settingsManager.settings.fattyMealFactor = expectedFattyMealFactor
+        settingsManager.settings.sweetMealFactor = expectedSweetMealFactor
+        settingsManager.settings.maxCarbs = expectedMaxCarbs
+
+        // Save settings to storage
+        fileStorage.save(settingsManager.settings, as: OpenAPS.Settings.settings)
+
+        // When
+        let (units, fraction, fattyMealFactor, sweetMealFactor, maxCarbs) = await getSettings()
+
+        // Then
+        #expect(units == expectedUnits, "Units should match settings")
+        #expect(fraction == expectedFraction, "Override factor should match settings")
+        #expect(fattyMealFactor == expectedFattyMealFactor, "Fatty meal factor should match settings")
+        #expect(sweetMealFactor == expectedSweetMealFactor, "Sweet meal factor should match settings")
+        #expect(maxCarbs == expectedMaxCarbs, "Max carbs should match settings")
+
+        // Cleanup - Restore original settings
+        settingsManager.settings = originalSettings
+        fileStorage.save(originalSettings, as: OpenAPS.Settings.settings)
+    }
+
+    @Test("Verify getCurrentSettingValue returns correct values based on time") func testGetCurrentSettingValue() async throws {
+        // STEP 1: Backup current settings
+        let originalBasalProfile = await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+        let originalCarbRatios = await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
+        let originalBGTargets = await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+        let originalISFValues = await fileStorage.retrieveAsync(
+            OpenAPS.Settings.insulinSensitivities,
+            as: InsulinSensitivities.self
+        )
+
+        // STEP 2: Setup test data with known values
+        // Note: Entries must be sorted by time for the algorithm to work correctly
+        let basalProfile = [
+            BasalProfileEntry(start: "00:00", minutes: 0, rate: 1.0), // 12:00 AM - 6:00 AM: 1.0
+            BasalProfileEntry(start: "06:00", minutes: 360, rate: 1.2), // 6:00 AM - 12:00 PM: 1.2
+            BasalProfileEntry(start: "12:00", minutes: 720, rate: 1.1), // 12:00 PM - 6:00 PM: 1.1
+            BasalProfileEntry(start: "18:00", minutes: 1080, rate: 0.9) // 6:00 PM - 12:00 AM: 0.9
+        ]
+
+        let carbRatios = CarbRatios(
+            units: .grams,
+            schedule: [
+                CarbRatioEntry(start: "00:00", offset: 0, ratio: 10), // 12:00 AM - 12:00 PM: 10
+                CarbRatioEntry(start: "12:00", offset: 720, ratio: 12) // 12:00 PM - 12:00 AM: 12
+            ]
+        )
+
+        let bgTargets = BGTargets(
+            units: .mgdL,
+            userPreferredUnits: .mgdL,
+            targets: [
+                BGTargetEntry(low: 100, high: 120, start: "00:00", offset: 0), // 12:00 AM - 8:00 AM: 100
+                BGTargetEntry(low: 90, high: 110, start: "08:00", offset: 480) // 8:00 AM - 12:00 AM: 90
+            ]
+        )
+
+        let isfValues = InsulinSensitivities(
+            units: .mgdL,
+            userPreferredUnits: .mgdL,
+            sensitivities: [
+                InsulinSensitivityEntry(sensitivity: 40, offset: 0, start: "00:00"), // 12:00 AM - 2:00 PM: 40
+                InsulinSensitivityEntry(sensitivity: 45, offset: 840, start: "14:00") // 2:00 PM - 12:00 AM: 45
+            ]
+        )
+
+        // STEP 3: Store test data
+        fileStorage.save(basalProfile, as: OpenAPS.Settings.basalProfile)
+        fileStorage.save(carbRatios, as: OpenAPS.Settings.carbRatios)
+        fileStorage.save(bgTargets, as: OpenAPS.Settings.bgTargets)
+        fileStorage.save(isfValues, as: OpenAPS.Settings.insulinSensitivities)
+
+        // STEP 4: Define test cases with specific times and expected values
+        // Format: (hour, minute, [setting type: expected value])
+        let testTimes: [(hour: Int, minute: Int, expected: [SettingType: Decimal])] = [
+            // Test midnight values (00:00)
+            (
+                hour: 0, minute: 0,
+                expected: [
+                    .basal: 1.0, // First basal rate
+                    .carbRatio: 10, // First carb ratio
+                    .bgTarget: 100, // First target
+                    .isf: 40 // First ISF
+                ]
+            ),
+            // Test mid-morning values (7:00)
+            (
+                hour: 7, minute: 0,
+                expected: [
+                    .basal: 1.2, // Second basal rate (after 6:00)
+                    .carbRatio: 10, // Still first carb ratio
+                    .bgTarget: 100, // Still first target
+                    .isf: 40 // Still first ISF
+                ]
+            ),
+            // Test afternoon values (15:00)
+            (
+                hour: 15, minute: 0,
+                expected: [
+                    .basal: 1.1, // Third basal rate (after 12:00)
+                    .carbRatio: 12, // Second carb ratio (after 12:00)
+                    .bgTarget: 90, // Second target
+                    .isf: 45 // Second ISF (after 14:00)
+                ]
+            )
+        ]
+
+        // STEP 5: Test each time point
+        for testTime in testTimes {
+            // Create a date object for the test time
+            let calendar = Calendar.current
+            var components = calendar.dateComponents([.year, .month, .day], from: Date())
+            components.hour = testTime.hour
+            components.minute = testTime.minute
+            components.second = 0
+            guard let testDate = calendar.date(from: components) else {
+                throw TestError("Failed to create test date")
+            }
+
+            // Test each setting type at this time
+            for (type, expectedValue) in testTime.expected {
+                // Get the actual value for this setting at the test time
+                let value = await getCurrentSettingValue(for: type, at: testDate)
+
+                // Compare with expected value
+                #expect(
+                    value == expectedValue,
+                    """
+                    Failed at \(testTime.hour):\(String(format: "%02d", testTime.minute))
+                    Setting: \(type)
+                    Expected: \(expectedValue)
+                    Actual: \(value)
+                    """
+                )
+            }
+        }
+
+        // STEP 6: Cleanup - Restore original settings
+        if let originalBasalProfile = originalBasalProfile {
+            fileStorage.save(originalBasalProfile, as: OpenAPS.Settings.basalProfile)
+        }
+        if let originalCarbRatios = originalCarbRatios {
+            fileStorage.save(originalCarbRatios, as: OpenAPS.Settings.carbRatios)
+        }
+        if let originalBGTargets = originalBGTargets {
+            fileStorage.save(originalBGTargets, as: OpenAPS.Settings.bgTargets)
+        }
+        if let originalISFValues = originalISFValues {
+            fileStorage.save(originalISFValues, as: OpenAPS.Settings.insulinSensitivities)
+        }
+    }
+}
+
+// Copied over from BolusCalculationManager as they are not included in the protocol definition (and I don´t want them to be included)
+
+extension BolusCalculatorTests {
+    private enum SettingType {
+        case basal
+        case carbRatio
+        case bgTarget
+        case isf
+    }
+
+    /// Retrieves current settings from the SettingsManager
+    /// - Returns: Tuple containing units, fraction, fattyMealFactor, sweetMealFactor, and maxCarbs settings
+    private func getSettings() async -> (
+        units: GlucoseUnits,
+        fraction: Decimal,
+        fattyMealFactor: Decimal,
+        sweetMealFactor: Decimal,
+        maxCarbs: Decimal
+    ) {
+        return (
+            units: settingsManager.settings.units,
+            fraction: settingsManager.settings.overrideFactor,
+            fattyMealFactor: settingsManager.settings.fattyMealFactor,
+            sweetMealFactor: settingsManager.settings.sweetMealFactor,
+            maxCarbs: settingsManager.settings.maxCarbs
+        )
+    }
+
+    /// Gets the current setting value for a specific setting type based on the time of day
+    /// - Parameter type: The type of setting to retrieve (basal, carbRatio, bgTarget, or isf)
+    /// - Returns: The current decimal value for the specified setting type
+    private func getCurrentSettingValue(for type: SettingType, at date: Date) async -> Decimal {
+        let calendar = Calendar.current
+        let midnight = calendar.startOfDay(for: date)
+        let minutesSinceMidnight = calendar.dateComponents([.minute], from: midnight, to: date).minute ?? 0
+
+        switch type {
+        case .basal:
+            let profile = await getBasalProfile()
+            return profile.last { $0.minutes <= minutesSinceMidnight }?.rate ?? 0
+
+        case .carbRatio:
+            let ratios = await getCarbRatios()
+            return ratios.schedule.last { $0.offset <= minutesSinceMidnight }?.ratio ?? 0
+
+        case .bgTarget:
+            let targets = await getBGTargets()
+            return targets.targets.last { $0.offset <= minutesSinceMidnight }?.low ?? 0
+
+        case .isf:
+            let sensitivities = await getISFValues()
+            return sensitivities.sensitivities.last { $0.offset <= minutesSinceMidnight }?.sensitivity ?? 0
+        }
+    }
+
+    /// Retrieves the pump settings from storage
+    /// - Returns: PumpSettings object containing pump configuration
+    private func getPumpSettings() async -> PumpSettings {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
+            ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+            ?? PumpSettings(insulinActionCurve: 10, maxBolus: 10, maxBasal: 2)
+    }
+
+    /// Retrieves the basal profile from storage
+    /// - Returns: Array of BasalProfileEntry objects
+    private func getBasalProfile() async -> [BasalProfileEntry] {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+            ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
+            ?? []
+    }
+
+    /// Retrieves carb ratios from storage
+    /// - Returns: CarbRatios object containing carb ratio schedule
+    private func getCarbRatios() async -> CarbRatios {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
+            ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
+            ?? CarbRatios(units: .grams, schedule: [])
+    }
+
+    /// Retrieves blood glucose targets from storage
+    /// - Returns: BGTargets object containing target schedule
+    private func getBGTargets() async -> BGTargets {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+            ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+            ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+    }
+
+    /// Retrieves insulin sensitivity factors from storage
+    /// - Returns: InsulinSensitivities object containing sensitivity schedule
+    private func getISFValues() async -> InsulinSensitivities {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
+            ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
+            ?? InsulinSensitivities(
+                units: .mgdL,
+                userPreferredUnits: .mgdL,
+                sensitivities: []
+            )
+    }
+}

+ 106 - 29
TrioTests/CalibrationsTests.swift

@@ -1,59 +1,136 @@
+import Foundation
 import Swinject
+import Testing
+
 @testable import Trio
-import XCTest
 
-class CalibrationsTests: XCTestCase, Injectable {
+@Suite("Calibration Service Tests") struct CalibrationTests: Injectable {
     let fileStorage = BaseFileStorage()
     @Injected() var calibrationService: CalibrationService!
     let resolver = TrioApp().resolver
 
-    override func setUp() {
+    init() {
         injectServices(resolver)
     }
 
-    func testCreateSimpleCalibration() {
-        // restore state so each test is independent
+    @Test("Can create simple calibration") func testCreateSimpleCalibration() {
+        // Given
         calibrationService.removeAllCalibrations()
-
         let calibration = Calibration(x: 100.0, y: 102.0)
-        calibrationService.addCalibration(calibration)
-
-        XCTAssertTrue(calibrationService.calibrations.isNotEmpty)
-
-        XCTAssertTrue(calibrationService.slope == 1)
 
-        XCTAssertTrue(calibrationService.intercept == 2)
+        // When
+        calibrationService.addCalibration(calibration)
 
-        XCTAssertTrue(calibrationService.calibrate(value: 104) == 106)
+        // Then
+        #expect(calibrationService.calibrations.isNotEmpty)
+        #expect(calibrationService.slope == 1)
+        #expect(calibrationService.intercept == 2)
+        #expect(calibrationService.calibrate(value: 104) == 106)
     }
 
-    func testCreateMultipleCalibration() {
-        // restore state so each test is independent
+    @Test("Can handle multiple calibrations") func testCreateMultipleCalibration() {
+        // Given
         calibrationService.removeAllCalibrations()
-
         let calibration = Calibration(x: 100.0, y: 120)
-        calibrationService.addCalibration(calibration)
-
         let calibration2 = Calibration(x: 120.0, y: 130.0)
+
+        // When
+        calibrationService.addCalibration(calibration)
         calibrationService.addCalibration(calibration2)
 
-        XCTAssertEqual(calibrationService.slope, 0.8, accuracy: 0.0001)
-        XCTAssertEqual(calibrationService.intercept, 37, accuracy: 0.0001)
-        XCTAssertEqual(calibrationService.calibrate(value: 80), 101, accuracy: 0.0001)
+        // Then
+        #expect(abs(calibrationService.slope - 0.8) < 0.0001)
+        #expect(abs(calibrationService.intercept - 37) < 0.0001)
+        #expect(abs(calibrationService.calibrate(value: 80) - 101) < 0.0001)
 
+        // When removing last
         calibrationService.removeLast()
+        #expect(calibrationService.calibrations.count == 1)
 
-        XCTAssertTrue(calibrationService.calibrations.count == 1)
-
+        // When removing all
         calibrationService.removeAllCalibrations()
-        XCTAssertTrue(calibrationService.calibrations.isEmpty)
+        #expect(calibrationService.calibrations.isEmpty)
     }
 
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
-    }
+    @Test("Handles calibration bounds correctly") func testCalibrationBounds() {
+        // Given
+        calibrationService.removeAllCalibrations()
 
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        // When no calibrations exist
+        #expect(calibrationService.slope == 1, "Default slope should be 1")
+        #expect(calibrationService.intercept == 0, "Default intercept should be 0")
+
+        // When adding extreme values
+        let extremeCalibration1 = Calibration(x: 0.0, y: 1000.0) // Should be clamped
+        let extremeCalibration2 = Calibration(x: 1000.0, y: 0.0) // Should be clamped
+
+        calibrationService.addCalibration(extremeCalibration1)
+        calibrationService.addCalibration(extremeCalibration2)
+
+        // Then check bounds
+        #expect(calibrationService.slope >= 0.8, "Slope should not be less than minimum")
+        #expect(calibrationService.slope <= 1.25, "Slope should not be more than maximum")
+        #expect(calibrationService.intercept >= -100, "Intercept should not be less than minimum")
+        #expect(calibrationService.intercept <= 100, "Intercept should not be more than maximum")
     }
 }
+
+// import Swinject
+// @testable import Trio
+// import XCTest
+//
+// class CalibrationsTests: XCTestCase, Injectable {
+//    let fileStorage = BaseFileStorage()
+//    @Injected() var calibrationService: CalibrationService!
+//    let resolver = TrioApp().resolver
+//
+//    override func setUp() {
+//        injectServices(resolver)
+//    }
+//
+//    func testCreateSimpleCalibration() {
+//        // restore state so each test is independent
+//        calibrationService.removeAllCalibrations()
+//
+//        let calibration = Calibration(x: 100.0, y: 102.0)
+//        calibrationService.addCalibration(calibration)
+//
+//        XCTAssertTrue(calibrationService.calibrations.isNotEmpty)
+//
+//        XCTAssertTrue(calibrationService.slope == 1)
+//
+//        XCTAssertTrue(calibrationService.intercept == 2)
+//
+//        XCTAssertTrue(calibrationService.calibrate(value: 104) == 106)
+//    }
+//
+//    func testCreateMultipleCalibration() {
+//        // restore state so each test is independent
+//        calibrationService.removeAllCalibrations()
+//
+//        let calibration = Calibration(x: 100.0, y: 120)
+//        calibrationService.addCalibration(calibration)
+//
+//        let calibration2 = Calibration(x: 120.0, y: 130.0)
+//        calibrationService.addCalibration(calibration2)
+//
+//        XCTAssertEqual(calibrationService.slope, 0.8, accuracy: 0.0001)
+//        XCTAssertEqual(calibrationService.intercept, 37, accuracy: 0.0001)
+//        XCTAssertEqual(calibrationService.calibrate(value: 80), 101, accuracy: 0.0001)
+//
+//        calibrationService.removeLast()
+//
+//        XCTAssertTrue(calibrationService.calibrations.count == 1)
+//
+//        calibrationService.removeAllCalibrations()
+//        XCTAssertTrue(calibrationService.calibrations.isEmpty)
+//    }
+//
+//    override func setUpWithError() throws {
+//        // Put setup code here. This method is called before the invocation of each test method in the class.
+//    }
+//
+//    override func tearDownWithError() throws {
+//        // Put teardown code here. This method is called after the invocation of each test method in the class.
+//    }
+// }

+ 45 - 0
TrioTests/CoreDataTests/CarbsStorageTests.swift

@@ -0,0 +1,45 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("CarbsStorage Tests") struct CarbsStorageTests: Injectable {
+    @Injected() var storage: CarbsStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override CarbsStorage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "CarbsStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(
+            storage is BaseCarbsStorage, "Storage should be of type BaseCarbsStorage"
+        )
+
+        // Verify we can access the update publisher
+        #expect(storage.updatePublisher != nil, "Update publisher should be available")
+    }
+}

+ 219 - 0
TrioTests/CoreDataTests/DeterminationStorageTests.swift

@@ -0,0 +1,219 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("Determination Storage Tests") struct DeterminationStorageTests: Injectable {
+    @Injected() var storage: DeterminationStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "DeterminationStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(storage is BaseDeterminationStorage, "Storage should be of type BaseDeterminationStorage")
+    }
+
+    @Test("Test fetchLastDeterminationObjectID with different predicates") func testFetchLastDeterminationWithPredicates() async throws {
+        // Given
+        let date = Date()
+        let id = UUID()
+
+        // Create a mock determination
+        await testContext.perform {
+            let determination = OrefDetermination(context: testContext)
+            determination.id = id
+            determination.deliverAt = date
+            determination.timestamp = date
+            determination.enacted = true
+            determination.isUploadedToNS = true
+            try? testContext.save()
+        }
+
+        // Tests with predicates that we use the most for this function
+        // 1. Test within 30 minutes
+        let results = try await storage
+            .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination)
+        #expect(results.count == 1, "Should find 1 determination within 30 minutes")
+        // Get NSManagedObjectID from exactDateResults
+        try await testContext.perform {
+            do {
+                guard let results = results.first,
+                      let object = try testContext.existingObject(with: results) as? OrefDetermination
+                else {
+                    throw TestError("Failed to fetch determination")
+                }
+                #expect(object.timestamp == date, "Determination within 30 minutes should have the same timestamp as date")
+                #expect(object.deliverAt == date, "Determination within 30 minutes should have the same deliverAt as date")
+                #expect(object.enacted == true, "Determination within 30 minutes should be enacted")
+                #expect(object.isUploadedToNS == true, "Determination within 30 minutes should be uploaded to NS")
+                #expect(object.id == id, "Determination within 30 minutes should have the same id")
+            } catch {
+                throw TestError("Failed to fetch determination")
+            }
+        }
+
+        // 2. Test enacted determinations
+        let enactedPredicate = NSPredicate.enactedDetermination
+        let enactedResults = try await storage.fetchLastDeterminationObjectID(predicate: enactedPredicate)
+        #expect(enactedResults.count == 1, "Should find 1 enacted determination")
+        // Get NSManagedObjectID from enactedResults
+        try await testContext.perform {
+            do {
+                guard let results = enactedResults.first,
+                      let object = try testContext.existingObject(with: results) as? OrefDetermination
+                else {
+                    throw TestError("Failed to fetch determination")
+                }
+                #expect(object.enacted == true, "Enacted determination should be enacted")
+                #expect(object.isUploadedToNS == true, "Enacted determination should be uploaded to NS")
+                #expect(object.id == id, "Enacted determination should have the same id")
+                #expect(object.timestamp == date, "Enacted determination should have the same timestamp")
+                #expect(object.deliverAt == date, "Enacted determination should have the same deliverAt")
+
+                // Delete the determination
+                testContext.delete(object)
+                try testContext.save()
+            } catch {
+                throw TestError("Failed to fetch determination")
+            }
+        }
+    }
+
+    @Test("Test complete forecast hierarchy prefetching") func testForecastHierarchyPrefetching() async throws {
+        // Given
+        let date = Date()
+        let forecastTypes = ["iob", "cob", "zt", "uam"]
+        var determinationId: NSManagedObjectID?
+        let expectedValuesPerForecast = 5
+
+        // STEP 1: Create test data
+        await testContext.perform {
+            let determination = OrefDetermination(context: testContext)
+            determination.id = UUID()
+            determination.deliverAt = date
+            determination.timestamp = date
+            determination.enacted = true
+
+            // Create all forecast types with values
+            for type in forecastTypes {
+                let forecast = Forecast(context: testContext)
+                forecast.id = UUID()
+                forecast.date = date
+                forecast.type = type
+                forecast.orefDetermination = determination
+
+                // Add test values with different patterns per type
+                for i in 0 ..< expectedValuesPerForecast {
+                    let value = ForecastValue(context: testContext)
+                    value.index = Int32(i)
+
+                    // Different value patterns for each type
+                    switch type {
+                    case "iob": value.value = Int32(100 + i * 10) // 100, 110, 120...
+                    case "cob": value.value = Int32(50 + i * 5) // 50, 55, 60...
+                    case "zt": value.value = Int32(80 + i * 8) // 80, 88, 96...
+                    case "uam": value.value = Int32(120 - i * 15) // 120, 105, 90...
+                    default: value.value = 0
+                    }
+
+                    value.forecast = forecast
+                }
+            }
+
+            try? testContext.save()
+            determinationId = determination.objectID
+        }
+
+        guard let determinationId = determinationId else {
+            throw TestError("Failed to create test data")
+        }
+
+        // STEP 2: Test hierarchy fetching
+        let hierarchy = try await storage.fetchForecastHierarchy(
+            for: determinationId,
+            in: testContext
+        )
+
+        // Test hierarchy structure
+        #expect(hierarchy.count == forecastTypes.count, "Should have correct number of forecasts")
+
+        // STEP 3: Test individual forecasts
+        for data in hierarchy {
+            let (id, forecast, values) = await storage.fetchForecastObjects(
+                for: data,
+                in: testContext
+            )
+
+            // Test basic structure
+            #expect(id != UUID(), "Should have valid UUID")
+            #expect(forecast != nil, "Forecast should exist")
+            #expect(values.count == expectedValuesPerForecast, "Should have correct number of values")
+
+            // Test forecast type and values
+            if let forecast = forecast {
+                #expect(forecastTypes.contains(forecast.type ?? ""), "Should have valid forecast type")
+
+                // Test value patterns
+                let sortedValues = values.sorted { $0.index < $1.index }
+                switch forecast.type {
+                case "iob":
+                    #expect(sortedValues.first?.value == 100, "IOB should start at 100")
+                    #expect(sortedValues.last?.value == 140, "IOB should end at 140")
+                case "cob":
+                    #expect(sortedValues.first?.value == 50, "COB should start at 50")
+                    #expect(sortedValues.last?.value == 70, "COB should end at 70")
+                case "zt":
+                    #expect(sortedValues.first?.value == 80, "ZT should start at 80")
+                    #expect(sortedValues.last?.value == 112, "ZT should end at 112")
+                case "uam":
+                    #expect(sortedValues.first?.value == 120, "UAM should start at 120")
+                    #expect(sortedValues.last?.value == 60, "UAM should end at 60")
+                default:
+                    break
+                }
+            }
+        }
+
+        // STEP 4: Test relationship integrity
+        try await testContext.perform {
+            do {
+                let determination = try testContext.existingObject(with: determinationId) as? OrefDetermination
+                let forecasts = Array(determination?.forecasts ?? [])
+
+                #expect(forecasts.count == forecastTypes.count, "Determination should have all forecasts")
+                #expect(
+                    forecasts.allSatisfy { Array($0.forecastValues ?? []).count == expectedValuesPerForecast },
+                    "Each forecast should have correct number of values"
+                )
+            } catch {
+                throw TestError("Failed to verify relationships: \(error)")
+            }
+        }
+    }
+}

+ 41 - 0
TrioTests/CoreDataTests/GlucoseStorageTests.swift

@@ -0,0 +1,41 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("GlucoseStorage Tests") struct GlucoseStorageTests: Injectable {
+    @Injected() var storage: GlucoseStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "GlucoseStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(storage is BaseGlucoseStorage, "Storage should be of type BaseGlucoseStorage")
+    }
+}

+ 41 - 0
TrioTests/CoreDataTests/OverrideStorageTests.swift

@@ -0,0 +1,41 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("Override Storage Tests") struct OverrideStorageTests: Injectable {
+    @Injected() var storage: OverrideStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "OverrideStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(storage is BaseOverrideStorage, "Storage should be of type BaseOverrideStorage")
+    }
+}

+ 288 - 0
TrioTests/CoreDataTests/PumpHistoryStorageTests.swift

@@ -0,0 +1,288 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import LoopKit
+@testable import Trio
+
+@Suite("PumpHistoryStorage Tests") struct PumpHistoryStorageTests: Injectable {
+    @Injected() var storage: PumpHistoryStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+    typealias PumpEvent = PumpEventStored.EventType
+
+    init() {
+        // Create test context
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override PumpHistoryStorage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "PumpHistoryStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(
+            storage is BasePumpHistoryStorage, "Storage should be of type BasePumpHistoryStorage"
+        )
+
+        // Verify we can access the update publisher
+        #expect(storage.updatePublisher != nil, "Update publisher should be available")
+    }
+
+    @Test("Test read and delete using generic CoreDataStack functions") func testFetchAndDeletePumpEvents() async throws {
+        // Given
+        let date = Date()
+
+        // Insert mock entry
+        let events: [LoopKit.NewPumpEvent] = [
+            LoopKit.NewPumpEvent(
+                date: date,
+                dose: LoopKit.DoseEntry(
+                    type: .bolus,
+                    startDate: date,
+                    value: 0.5,
+                    unit: .units,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: false,
+                    manuallyEntered: false,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus for Fetch",
+                type: .bolus
+            )
+        ]
+
+        // Store test event
+        try await storage.storePumpEvents(events)
+
+        // When - Fetch events with our generic fetch function
+        let fetchedEvents = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(
+                format: "type == %@ AND timestamp == %@",
+                PumpEvent.bolus.rawValue,
+                date as NSDate
+            ),
+            key: "timestamp",
+            ascending: false
+        )
+
+        guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
+
+        // Then
+        #expect(fetchedEvents.count == 1, "Should have found exactly one event")
+        let fetchedEvent = fetchedEvents.first
+        #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
+        #expect(fetchedEvent?.bolus?.amount as? Decimal == 0.5, "Bolus amount should be 0.5")
+        #expect(
+            abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
+            "Timestamp should match"
+        )
+
+        // When - Delete event
+        if let fetchedEvent = fetchedEvent {
+            await coreDataStack.deleteObject(identifiedBy: fetchedEvent.objectID)
+        }
+
+        // Then - Verify deletion
+        let eventsAfterDeletion = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(
+                format: "type == %@ AND timestamp == %@",
+                PumpEvent.bolus.rawValue,
+                date as NSDate
+            ),
+            key: "timestamp",
+            ascending: false
+        )
+
+        guard let eventsAfterDeletion = eventsAfterDeletion as? [PumpEventStored] else { return }
+
+        #expect(eventsAfterDeletion.isEmpty, "Should have no events after deletion")
+    }
+
+    @Test("Test store function in PumpHistoryStorage") func testStorePumpEvents() async throws {
+        // Given
+        let date = Date()
+        let tenMinAgo = date.addingTimeInterval(-10.minutes.timeInterval)
+        let halfHourInFuture = date.addingTimeInterval(30.minutes.timeInterval)
+
+        // Get initial entries to compare to final entries later
+        let initialEntries = try await testContext.perform {
+            try testContext.fetch(PumpEventStored.fetchRequest())
+        }
+
+        // Create 2 test events, 1 bolus + 1 temp basal event
+        let events: [LoopKit.NewPumpEvent] = [
+            // SMB
+            LoopKit.NewPumpEvent(
+                date: tenMinAgo,
+                dose: LoopKit.DoseEntry(
+                    type: .bolus,
+                    startDate: tenMinAgo,
+                    value: 0.4,
+                    unit: .units,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus",
+                type: .bolus
+            ),
+            // Temp Basal event
+            LoopKit.NewPumpEvent(
+                date: date,
+                dose: LoopKit.DoseEntry(
+                    type: .tempBasal,
+                    startDate: date,
+                    endDate: halfHourInFuture,
+                    value: 1.2,
+                    unit: .unitsPerHour,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: true
+                ),
+                raw: Data(),
+                title: "Test Temp Basal",
+                type: .tempBasal
+            )
+        ]
+
+        // When
+        // Store in our in-memory PumphistoryStorage
+        try await storage.storePumpEvents(events)
+
+        // Then
+        // Fetch all events after storing
+        let finalEntries = try await testContext.perform {
+            try testContext.fetch(PumpEventStored.fetchRequest())
+        }
+
+        // Verify there were no initial entries
+        #expect(initialEntries.isEmpty, "There should be no initial entries")
+
+        // Verify count increased by 2
+        #expect(finalEntries.count == initialEntries.count + 2, "Should have added 2 new events")
+
+        // Verify bolus event
+        let bolusEvent = finalEntries.first {
+            $0.type == PumpEvent.bolus.rawValue &&
+                abs($0.timestamp?.timeIntervalSince(tenMinAgo) ?? 1) < 1
+        }
+        #expect(bolusEvent != nil, "Should have found bolus event")
+        #expect(bolusEvent?.bolus?.amount as? Decimal == 0.4, "Bolus amount should be 0.4")
+        #expect(bolusEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
+        #expect(bolusEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
+        #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
+        #expect(bolusEvent?.bolus?.isSMB == true, "Should be a SMB")
+        #expect(bolusEvent?.bolus?.isExternal == false, "Should not be external insulin")
+
+        // Verify temp basal event
+        let tempBasalEvent = finalEntries.first {
+            $0.type == PumpEvent.tempBasal.rawValue &&
+                abs($0.timestamp?.timeIntervalSince(date) ?? 1) < 1
+        }
+        #expect(tempBasalEvent != nil, "Should have found temp basal event")
+        #expect(tempBasalEvent?.tempBasal?.rate as? Decimal == 1.2, "Temp basal rate should be 1.2")
+        #expect(tempBasalEvent?.tempBasal?.duration == 30, "Temp basal duration should be 30 minutes")
+        #expect(tempBasalEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
+        #expect(tempBasalEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
+        #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
+    }
+
+    @Test("Test store function for manual boluses") func testStorePumpEventsWithManualBoluses() async throws {
+        // Given
+        let date = Date()
+
+        // Insert mock entry
+        let events: [LoopKit.NewPumpEvent] = [
+            LoopKit.NewPumpEvent(
+                date: date,
+                dose: LoopKit.DoseEntry(
+                    type: .bolus,
+                    startDate: date,
+                    value: 4,
+                    unit: .units,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: false,
+                    manuallyEntered: false,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus",
+                type: .bolus
+            )
+        ]
+
+        // Store test event and wait for storage to complete the task
+        try await storage.storePumpEvents(events)
+
+        // When - Fetch events with our generic fetch function
+        let fetchedEvents = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(
+                format: "type == %@ AND timestamp == %@",
+                PumpEvent.bolus.rawValue,
+                date as NSDate
+            ),
+            key: "timestamp",
+            ascending: false
+        )
+
+        guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
+
+        // Then
+        #expect(fetchedEvents.count == 1, "Should have found exactly one event")
+        let fetchedEvent = fetchedEvents.first
+        #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
+        #expect(fetchedEvent?.bolus?.amount as? Decimal == 4, "Bolus amount should be 4 U")
+        #expect(
+            abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
+            "Timestamp should match"
+        )
+        #expect(fetchedEvent?.bolus?.isSMB == false, "Should not be a SMB")
+        #expect(fetchedEvent?.bolus?.isExternal == false, "Should not be external Insulin")
+        #expect(fetchedEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
+        #expect(fetchedEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
+        #expect(fetchedEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
+    }
+}

+ 42 - 0
TrioTests/CoreDataTests/TempTargetStorageTests.swift

@@ -0,0 +1,42 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("TempTargetStorage Tests") struct TempTargetsStorageTests: Injectable {
+    @Injected() var storage: TempTargetsStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override TempTargetStorage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "TempTargetsStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(
+            storage is BaseTempTargetsStorage, "Storage should be of type BaseTempTargetsStorage"
+        )
+    }
+}

+ 44 - 0
TrioTests/CoreDataTests/TestAssembly.swift

@@ -0,0 +1,44 @@
+import CoreData
+import Foundation
+import Swinject
+@testable import Trio
+
+class TestAssembly: Assembly {
+    private let testContext: NSManagedObjectContext
+
+    init(testContext: NSManagedObjectContext) {
+        self.testContext = testContext
+    }
+
+    func assemble(container: Container) {
+        // Override PumpHistoryStorage registration for tests
+        container.register(PumpHistoryStorage.self) { r in
+            BasePumpHistoryStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override DeterminationStorage registration for tests
+        container.register(DeterminationStorage.self) { r in
+            BaseDeterminationStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override CarbsStorage registration for tests
+        container.register(CarbsStorage.self) { r in
+            BaseCarbsStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override GlucoseStorage registration for tests
+        container.register(GlucoseStorage.self) { r in
+            BaseGlucoseStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override TempTargetStorage registration for tests
+        container.register(TempTargetsStorage.self) { r in
+            BaseTempTargetsStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override OverrideStorage registration for tests
+        container.register(OverrideStorage.self) { r in
+            BaseOverrideStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+    }
+}

+ 128 - 12
TrioTests/FileStorageTests.swift

@@ -1,26 +1,142 @@
+import Foundation
+import Testing
+
 @testable import Trio
-import XCTest
 
-class FileStorageTests: XCTestCase {
-    let fileStorage = BaseFileStorage()
+@Suite("File Storage Tests") struct FileStorageTests {
+    let storage = BaseFileStorage()
 
     struct DummyObject: JSON, Equatable {
         let id: String
         let value: Decimal
     }
 
-    func testFileStorageTrio() {
-        let dummyObject = DummyObject(id: "21342Z", value: 78.2)
-        fileStorage.save(dummyObject, as: "dummyObject")
-        let dummyObjectRetrieve = fileStorage.retrieve("dummyObject", as: DummyObject.self)
-        XCTAssertTrue(dummyObject == dummyObjectRetrieve)
+    @Test("Can save and retrieve object") func testSaveAndRetrieve() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+
+        // When
+        storage.save(dummy, as: "dummy")
+        let retrieved = storage.retrieve("dummy", as: DummyObject.self)
+
+        // Then
+        #expect(retrieved == dummy)
+    }
+
+    @Test("Can save and retrieve async") func testAsyncSaveAndRetrieve() async {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+
+        // When
+        await storage.saveAsync(dummy, as: "dummy_async")
+        let retrieved = await storage.retrieveAsync("dummy_async", as: DummyObject.self)
+
+        // Then
+        #expect(retrieved == dummy)
+    }
+
+    @Test("Can append single value") func testAppendSingleValue() {
+        // Given
+        let dummy1 = DummyObject(id: "1", value: 10.0)
+        let dummy2 = DummyObject(id: "2", value: 20.0)
+
+        // When
+        storage.save([dummy1], as: "dummies")
+        storage.append(dummy2, to: "dummies")
+
+        // Then
+        let retrieved = storage.retrieve("dummies", as: [DummyObject].self)
+        #expect(retrieved?.count == 2)
+        #expect(retrieved?.contains(dummy1) == true)
+        #expect(retrieved?.contains(dummy2) == true)
+    }
+
+    @Test("Can append multiple values") func testAppendMultipleValues() {
+        // Given
+        let dummy1 = DummyObject(id: "1", value: 10.0)
+        let newDummies = [
+            DummyObject(id: "2", value: 20.0),
+            DummyObject(id: "3", value: 30.0)
+        ]
+
+        // When
+        storage.save([dummy1], as: "dummies_multiple")
+        storage.append(newDummies, to: "dummies_multiple")
+
+        // Then
+        let retrieved = storage.retrieve("dummies_multiple", as: [DummyObject].self)
+        #expect(retrieved?.count == 3)
+    }
+
+    @Test("Can append unique values by key path") func testAppendUniqueByKeyPath() {
+        // Given
+        let dummy1 = DummyObject(id: "1", value: 10.0)
+        let dummy2 = DummyObject(id: "1", value: 20.0) // Same id
+
+        // When
+        storage.save([dummy1], as: "unique_dummies")
+        storage.append(dummy2, to: "unique_dummies", uniqBy: \.id)
+
+        // Then
+        let retrieved = storage.retrieve("unique_dummies", as: [DummyObject].self)
+        #expect(retrieved?.count == 1, "Should not append duplicate id")
+    }
+
+    @Test("Can remove file") func testRemoveFile() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+        storage.save(dummy, as: "to_delete")
+
+        // When
+        storage.remove("to_delete")
+
+        // Then
+        let retrieved = storage.retrieve("to_delete", as: DummyObject.self)
+        #expect(retrieved == nil)
+    }
+
+    @Test("Can rename file") func testRenameFile() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+        storage.save(dummy, as: "old_name")
+
+        // When
+        storage.rename("old_name", to: "new_name")
+
+        // Then
+        let oldRetrieved = storage.retrieve("old_name", as: DummyObject.self)
+        let newRetrieved = storage.retrieve("new_name", as: DummyObject.self)
+
+        #expect(oldRetrieved == nil)
+        #expect(newRetrieved == dummy)
     }
 
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
+    @Test("Can execute transaction") func testTransaction() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+
+        // When
+        storage.transaction { storage in
+            storage.save(dummy, as: "transaction_test")
+        }
+
+        // Then
+        let retrieved = storage.retrieve("transaction_test", as: DummyObject.self)
+        #expect(retrieved == dummy)
     }
 
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
+    @Test("Can parse mmol/L settings to mg/dL") func testParseSettingsToMgdL() {
+        // Given
+        var preferences = Preferences()
+        preferences.threshold_setting = 5.5 // mmol/L
+        storage.save(preferences, as: OpenAPS.Settings.preferences)
+
+        // When
+        let wasParsed = storage.parseOnFileSettingsToMgdL()
+
+        // Then
+        #expect(wasParsed == true)
+        let parsed = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
+        #expect(parsed?.threshold_setting == 100) // default mg/dL value
     }
 }

+ 56 - 36
TrioTests/PluginManagerTests.swift

@@ -1,71 +1,91 @@
+import Foundation
 import Swinject
+import Testing
 @testable import Trio
-import XCTest
 
-class PluginManagerTests: XCTestCase, Injectable {
+@Suite("Plugin Manager Tests") struct PluginManagerTests: Injectable {
     let fileStorage = BaseFileStorage()
     @Injected() var pluginManager: PluginManager!
     let resolver = TrioApp().resolver
 
-    override func setUp() {
+    init() {
         injectServices(resolver)
     }
 
-    func testCGMManagerLoad() {
+    @Test("Can load CGM managers") func testCGMManagerLoad() {
+        // Given
         let cgmLoopManagers = pluginManager.availableCGMManagers
-        XCTAssertNotNil(cgmLoopManagers)
-        XCTAssertTrue(!cgmLoopManagers.isEmpty)
+
+        // Then
+        #expect(!cgmLoopManagers.isEmpty, "Should have available CGM managers")
+
+        // When loading valid CGM manager
         if let cgmLoop = cgmLoopManagers.first {
             let cgmLoopManager = pluginManager.getCGMManagerTypeByIdentifier(cgmLoop.identifier)
-            XCTAssertNotNil(cgmLoopManager)
-        } else {
-            XCTFail("Not found CGM loop manager")
+            #expect(cgmLoopManager != nil, "Should load valid CGM manager")
         }
-        /// try to load a Pump manager with a CGM identifier
+
+        // When trying to load CGM manager with pump identifier
         if let cgmLoop = cgmLoopManagers.last {
-            let cgmLoopManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
-            XCTAssertNil(cgmLoopManager)
-        } else {
-            XCTFail("Not found CGM loop manager")
+            let invalidManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
+            #expect(invalidManager == nil, "Should not load CGM manager with pump identifier")
         }
     }
 
-    func testPumpManagerLoad() {
+    @Test("Can load pump managers") func testPumpManagerLoad() {
+        // Given
         let pumpLoopManagers = pluginManager.availablePumpManagers
-        XCTAssertNotNil(pumpLoopManagers)
-        XCTAssertTrue(!pumpLoopManagers.isEmpty)
+
+        // Then
+        #expect(!pumpLoopManagers.isEmpty, "Should have available pump managers")
+
+        // When loading valid pump manager
         if let pumpLoop = pumpLoopManagers.first {
             let pumpLoopManager = pluginManager.getPumpManagerTypeByIdentifier(pumpLoop.identifier)
-            XCTAssertNotNil(pumpLoopManager)
-        } else {
-            XCTFail("Not found pump loop manager")
+            #expect(pumpLoopManager != nil, "Should load valid pump manager")
         }
-        /// try to load a CGM manager with a pump identifier
+
+        // When trying to load pump manager with CGM identifier
         if let pumpLoop = pumpLoopManagers.last {
-            let pumpLoopManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
-            XCTAssertNil(pumpLoopManager)
-        } else {
-            XCTFail("Not found pump loop manager")
+            let invalidManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
+            #expect(invalidManager == nil, "Should not load pump manager with CGM identifier")
         }
     }
 
-    func testServiceManagerLoad() {
+    @Test("Can load service managers") func testServiceManagerLoad() {
+        // Given
         let serviceManagers = pluginManager.availableServices
-        XCTAssertNotNil(serviceManagers)
-        XCTAssertTrue(!serviceManagers.isEmpty)
+
+        // Then
+        #expect(!serviceManagers.isEmpty, "Should have available services")
+
+        // When
         if let serviceLoop = serviceManagers.first {
             let serviceManager = pluginManager.getServiceTypeByIdentifier(serviceLoop.identifier)
-            XCTAssertNotNil(serviceManager)
-        } else {
-            XCTFail("Not found Service loop manager")
+            #expect(serviceManager != nil, "Should load valid service manager")
         }
     }
 
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
-    }
+    @Test("Available managers have valid descriptors") func testManagerDescriptors() {
+        // Given/When
+        let pumpManagers = pluginManager.availablePumpManagers
+        let cgmManagers = pluginManager.availableCGMManagers
+        let serviceManagers = pluginManager.availableServices
+
+        // Then
+        for manager in pumpManagers {
+            #expect(!manager.identifier.isEmpty, "Pump manager should have non-empty identifier")
+            #expect(!manager.localizedTitle.isEmpty, "Pump manager should have non-empty title")
+        }
 
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        for manager in cgmManagers {
+            #expect(!manager.identifier.isEmpty, "CGM manager should have non-empty identifier")
+            #expect(!manager.localizedTitle.isEmpty, "CGM manager should have non-empty title")
+        }
+
+        for manager in serviceManagers {
+            #expect(!manager.identifier.isEmpty, "Service should have non-empty identifier")
+            #expect(!manager.localizedTitle.isEmpty, "Service should have non-empty title")
+        }
     }
 }

+ 10 - 0
TrioTests/TestError.swift

@@ -0,0 +1,10 @@
+import Foundation
+
+// Custom error type for test failures
+struct TestError: Error {
+    let message: String
+
+    init(_ message: String) {
+        self.message = message
+    }
+}