Преглед изворни кода

Add meal replay + fix bug

This PR adds meal replay capabilities. In the process we fixed two bugs:

- Convert the testing JS to use trio_ instead of freeaps_
- Pass in the correct time to the IoB function
Sam King пре 11 месеци
родитељ
комит
900ec67f13

+ 6 - 2
Trio.xcodeproj/project.pbxproj

@@ -201,6 +201,7 @@
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
+		3B0B4E6C2DE1439F005C6627 /* LockedResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */; };
 		3B139EF32DF06CE100D40797 /* AutosensGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B139EF22DF06CDC00D40797 /* AutosensGenerator.swift */; };
 		3B1C5C292D68E1E3004E9273 /* IobCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C242D68E1E3004E9273 /* IobCalculation.swift */; };
 		3B1C5C2A2D68E1E3004E9273 /* IobGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C262D68E1E3004E9273 /* IobGenerator.swift */; };
@@ -215,13 +216,13 @@
 		3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C382D68E269004E9273 /* IobTotalTests.swift */; };
 		3B1C5C472D68E269004E9273 /* IobCalculateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */; };
 		3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C362D68E269004E9273 /* IobHistoryTests.swift */; };
-		3B0B4E6C2DE1439F005C6627 /* LockedResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
 		3B3B57C92DA07B3400849D16 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */; };
 		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
 		3B4550532D862C0000551B0D /* PumpHistoryEvent+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */; };
 		3B47C6102DA0A28F00B0E5EF /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */; };
+		3B4821822E080CAE00F0DD17 /* HttpFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4821812E080CAE00F0DD17 /* HttpFiles.swift */; };
 		3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; };
 		3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; };
@@ -1118,6 +1119,7 @@
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMSettingsProvider.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
+		3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedResolver.swift; sourceTree = "<group>"; };
 		3B139EF22DF06CDC00D40797 /* AutosensGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensGenerator.swift; sourceTree = "<group>"; };
 		3B1C5C242D68E1E3004E9273 /* IobCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobCalculation.swift; sourceTree = "<group>"; };
 		3B1C5C252D68E1E3004E9273 /* IobError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobError.swift; sourceTree = "<group>"; };
@@ -1132,12 +1134,12 @@
 		3B1C5C382D68E269004E9273 /* IobTotalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobTotalTests.swift; sourceTree = "<group>"; };
 		3B1C5C3D2D68E269004E9273 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
 		3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobJsonTypes.swift; sourceTree = "<group>"; };
-		3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedResolver.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
 		3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
 		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
 		3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpHistoryEvent+Duplicates.swift"; sourceTree = "<group>"; };
+		3B4821812E080CAE00F0DD17 /* HttpFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpFiles.swift; sourceTree = "<group>"; };
 		3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DanaKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -2783,6 +2785,7 @@
 			isa = PBXGroup;
 			children = (
 				3B1C5C3D2D68E269004E9273 /* Extensions.swift */,
+				3B4821812E080CAE00F0DD17 /* HttpFiles.swift */,
 				3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */,
 				3BF92F372D86E106006B545A /* OpenAPSFixed.swift */,
 				3B8B5D3D2DF5240600365ED3 /* TimeZoneForTests.swift */,
@@ -5103,6 +5106,7 @@
 				3B1C5C432D68E269004E9273 /* Extensions.swift in Sources */,
 				3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */,
 				3BF92F382D86E10B006B545A /* OpenAPSFixed.swift in Sources */,
+				3B4821822E080CAE00F0DD17 /* HttpFiles.swift in Sources */,
 				3B1C5C442D68E269004E9273 /* IobJsonTypes.swift in Sources */,
 				3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */,
 				3BE2F1EA2E031951009E2900 /* MealCobBucketingTests.swift in Sources */,

+ 2 - 1
Trio/Sources/APS/OpenAPSSwift/Meal/MealCob.swift

@@ -30,6 +30,7 @@ struct MealCob {
     ///
     /// This is the main COB detection algorithm entry point
     static func detectCarbAbsorption(
+        clock: Date,
         glucose: [BloodGlucose],
         pumpHistory: [PumpHistoryEvent],
         basalProfile: [BasalProfileEntry],
@@ -40,7 +41,7 @@ struct MealCob {
         let treatments = try IobHistory.calcTempTreatments(
             history: pumpHistory.map { $0.computedEvent() },
             profile: profile,
-            clock: mealDate,
+            clock: clock,
             autosens: nil,
             zeroTempDuration: nil
         )

+ 4 - 1
Trio/Sources/APS/OpenAPSSwift/Meal/MealTotal.swift

@@ -15,6 +15,7 @@ struct ComputedCarbs: Codable {
 struct IOBInput {
     let profile: Profile
     let history: [PumpHistoryEvent]
+    let clock: Date
 }
 
 struct COBInputs {
@@ -78,7 +79,7 @@ enum MealTotal {
         let mealCarbTime: TimeInterval = time.timeIntervalSince1970
         var lastCarbTime: TimeInterval = 0
 
-        let iobInputs = IOBInput(profile: profile, history: pumpHistory)
+        let iobInputs = IOBInput(profile: profile, history: pumpHistory, clock: time)
         var cobInputs = COBInputs(
             glucoseData: glucose,
             iobInputs: iobInputs,
@@ -110,6 +111,7 @@ enum MealTotal {
                     lastCarbTime = max(lastCarbTime, treatmentTime)
 
                     let myCarbsAbsorbed = try MealCob.detectCarbAbsorption(
+                        clock: cobInputs.iobInputs.clock,
                         glucose: cobInputs.glucoseData,
                         pumpHistory: cobInputs.iobInputs.history,
                         basalProfile: cobInputs.basalProfile,
@@ -143,6 +145,7 @@ enum MealTotal {
         /// omiting maxCOB check here, the setting is not Optional in Swift and must be part of profile
 
         let finalCobResult = try MealCob.detectCarbAbsorption(
+            clock: cobInputs.iobInputs.clock,
             glucose: cobInputs.glucoseData,
             pumpHistory: cobInputs.iobInputs.history,
             basalProfile: cobInputs.basalProfile,

+ 3 - 9
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -40,18 +40,12 @@ import Testing
     // Note: This test case has a memory leak so limit your inputs
     // to about 250 files at a time
     @Test(
-        "should produce same results for fixed JS and different for bundle JS",
+        "IoB should produce same results for fixed JS and different for bundle JS",
         .enabled(if: false)
     ) func replayErrorInputs() async throws {
-        let url = URL(string: "http://localhost:8123/list")!
-        let (data, _) = try await URLSession.shared.data(from: url)
-        let files = try JSONDecoder().decode([String].self, from: data)
-        let fileDataDecoder = JSONDecoder()
-        fileDataDecoder.dateDecodingStrategy = .secondsSince1970
+        let files = try await HttpFiles.listFiles()
         for filePath in files {
-            let dataUrl = URL(string: "http://localhost:8123\(filePath)")!
-            let (data, _) = try await URLSession.shared.data(from: dataUrl)
-            let algorithmComparison = try fileDataDecoder.decode(AlgorithmComparison.self, from: data)
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
             print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
             guard let iobInputs = algorithmComparison.iobInput else {
                 print("Skipping, no iobInputs found")

+ 7 - 0
TrioTests/OpenAPSSwiftTests/MealCobTests.swift

@@ -50,6 +50,7 @@ import Testing
 
         // Test with carbImpactTime
         var result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -62,6 +63,7 @@ import Testing
 
         // Test without carbImpactTime
         result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -86,6 +88,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -110,6 +113,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -150,6 +154,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -183,6 +188,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: carbImpactTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,
@@ -213,6 +219,7 @@ import Testing
         let pumpHistory: [PumpHistoryEvent] = []
 
         let result = try MealCob.detectCarbAbsorption(
+            clock: mealTime, // no pump events, set to whatever
             glucose: glucoseData,
             pumpHistory: pumpHistory,
             basalProfile: basalProfile,

+ 124 - 1
TrioTests/OpenAPSSwiftTests/MealJsonTests.swift

@@ -2,7 +2,9 @@ import Foundation
 import Testing
 @testable import Trio
 
-@Suite("Meal testing using JSON inputs") struct MealJsonTests {
+@Suite("Meal testing using JSON inputs", .serialized) struct MealJsonTests {
+    let timeZoneForTests = TimeZoneForTests()
+
     @Test("Test against simulator inputs") func simulatorInputs() throws {
         let testBundle = Bundle(for: BundleReference.self)
         let path = testBundle.path(forResource: "meal-input-sim", ofType: "json")!
@@ -46,4 +48,125 @@ import Testing
         #expect(mealResult?.minDeviation == mealResultFromJs.minDeviation)
         #expect(mealResult!.slopeFromMinDeviation.isWithin(0.01, of: mealResultFromJs.slopeFromMinDeviation))
     }
+
+    @Test(
+        "Meal should produce same results for fixed JS",
+        .enabled(if: true)
+    ) func replayErrorInputs() async throws {
+        let files = try await HttpFiles.listFiles()
+        for filePath in files {
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
+            print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
+            guard let mealInputs = algorithmComparison.mealInput else {
+                print("Skipping, no mealInputs found")
+                if let str = algorithmComparison.comparisonError {
+                    print(str)
+                }
+                if let str = algorithmComparison.swiftException {
+                    print(str)
+                }
+                continue
+            }
+
+            timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+
+            try await checkFixedJsAgainstSwift(mealInputs: mealInputs)
+
+            timeZoneForTests.resetTimezone()
+        }
+    }
+
+    func checkFixedJsAgainstSwift(mealInputs: MealInputs) async throws {
+        let openAps = OpenAPSFixed()
+        let (mealResultSwift, _) = OpenAPSSwift.meal(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        let mealResultJavascript = await openAps.mealJavascript(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        let comparison = JSONCompare.createComparison(
+            function: .meal,
+            swift: mealResultSwift,
+            swiftDuration: 0.1,
+            javascript: mealResultJavascript,
+            javascriptDuration: 0.1,
+            iobInputs: nil,
+            mealInputs: nil
+        )
+
+        if comparison.resultType == .valueDifference {
+            print(comparison.differences!.prettyPrintedJSON!)
+        }
+
+        if comparison.resultType != .matching {
+            print("REPLAY ERROR: Fixed JS didn't match")
+        }
+
+        #expect(comparison.resultType == .matching)
+    }
+
+    @Test("Format inputs for running in JS", .enabled(if: true)) func formatInputs() async throws {
+        let openAps = OpenAPSFixed()
+
+        // this test is meant for one-off analysis so it's ok to hard code
+        // a file, just make sure to _not_ check in updates to this to
+        // avoid polluting our change logs
+        let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/02273a81-c2ed-461b-8d4e-b9b085227f61.1.json")
+        let mealInputs = algorithmComparison.mealInput!
+
+        let encoder = JSONCoding.encoder
+        let output = try encoder.encode(mealInputs)
+
+        let sharedDir = FileManager.default.temporaryDirectory
+        let outputURL = sharedDir.appendingPathComponent("meal_error_inputs.json")
+        // Print the path so you can find it
+        print("Writing to: \(outputURL.path)")
+        try output.write(to: outputURL)
+
+        let (mealResultSwift, _) = OpenAPSSwift.meal(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        print("Swift result")
+        switch mealResultSwift {
+        case let .success(rawJson):
+            print(rawJson)
+        case let .failure(error):
+            print(error.localizedDescription)
+        }
+
+        let mealResultJavascript = await openAps.mealJavascript(
+            pumphistory: mealInputs.pumpHistory,
+            profile: try JSONBridge.to(mealInputs.profile),
+            basalProfile: mealInputs.basalProfile,
+            clock: mealInputs.clock,
+            carbs: mealInputs.carbs,
+            glucose: mealInputs.glucose
+        )
+
+        print("Fixed JS result")
+        switch mealResultJavascript {
+        case let .success(rawJson):
+            print(rawJson)
+        case let .failure(error):
+            print(error.localizedDescription)
+        }
+    }
 }

Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-core.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-prep.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/basal-set-temp.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/determine-basal.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/glucose-get-last.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-calculate.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-history.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-total.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/meal.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/profile.js


+ 23 - 0
TrioTests/OpenAPSSwiftTests/utils/HttpFiles.swift

@@ -0,0 +1,23 @@
+import Foundation
+@testable import Trio
+
+/// Helper struct to download files from localhost via HTTP. Must have a HTTP server
+/// running on port 8123 that supports listing files and downloading files
+///
+/// This struct is only useful during testing as it is missing a number of error checks
+struct HttpFiles {
+    static func listFiles() async throws -> [String] {
+        let url = URL(string: "http://localhost:8123/list")!
+        let (data, _) = try await URLSession.shared.data(from: url)
+        let files = try JSONDecoder().decode([String].self, from: data)
+        return files
+    }
+
+    static func downloadFile(at: String) async throws -> AlgorithmComparison {
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .secondsSince1970
+        let dataUrl = URL(string: "http://localhost:8123\(at)")!
+        let (data, _) = try await URLSession.shared.data(from: dataUrl)
+        return try decoder.decode(AlgorithmComparison.self, from: data)
+    }
+}

+ 34 - 0
TrioTests/OpenAPSSwiftTests/utils/OpenAPSFixed.swift

@@ -39,6 +39,40 @@ final class OpenAPSFixed {
         return result
     }
 
+    func mealJavascript(
+        pumphistory: JSON,
+        profile: JSON,
+        basalProfile: JSON,
+        clock: JSON,
+        carbs: JSON,
+        glucose: JSON
+    ) async -> OrefFunctionResult {
+        let testBundle = Bundle(for: OpenAPSFixed.self)
+        do {
+            let result = try await withCheckedThrowingContinuation { continuation in
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: "prepare/log.js"),
+                        Script.fromTestingBundle(name: "meal.js", bundle: testBundle),
+                        Script(name: "prepare/meal.js")
+                    ])
+                    let result = worker.call(function: "generate", with: [
+                        pumphistory,
+                        profile,
+                        clock,
+                        glucose,
+                        basalProfile,
+                        carbs
+                    ])
+                    continuation.resume(returning: result)
+                }
+            }
+            return .success(result)
+        } catch {
+            return .failure(error)
+        }
+    }
+
     func iobJavascript(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async -> OrefFunctionResult {
         do {
             let testBundle = Bundle(for: OpenAPSFixed.self)