@@ -74,10 +74,10 @@ struct FutureCarbsConditionTests {
7474 #expect( Storage . shared. pendingFutureCarbs. value. isEmpty)
7575 }
7676
77- // MARK: - 4. Beyond lookahead — carb ignored
77+ // MARK: - 4. Beyond lookahead — tracked but does not fire
7878
79- @Test ( " #beyond lookahead — carb ignored " )
80- func beyondLookaheadIgnored ( ) {
79+ @Test ( " #beyond lookahead — tracked but does not fire " )
80+ func beyondLookaheadTrackedButNoFire ( ) {
8181 resetPending ( )
8282 let now = Date ( )
8383 let alarm = Alarm . futureCarbs ( threshold: 45 )
@@ -86,7 +86,9 @@ struct FutureCarbsConditionTests {
8686 let result = cond. evaluate ( alarm: alarm, data: data, now: now)
8787
8888 #expect( !result)
89- #expect( Storage . shared. pendingFutureCarbs. value. isEmpty)
89+ // Carb is tracked (to prevent re-observation with fresh observedAt)
90+ // but will never fire because original distance > lookahead
91+ #expect( Storage . shared. pendingFutureCarbs. value. count == 1 )
9092 }
9193
9294 // MARK: - 5. Below min grams — carb ignored
@@ -212,4 +214,87 @@ struct FutureCarbsConditionTests {
212214 _ = cond. evaluate ( alarm: alarm, data: data, now: now)
213215 #expect( Storage . shared. pendingFutureCarbs. value. count == 1 )
214216 }
217+
218+ // MARK: - 11. Sliding window — carb outside lookahead never fires
219+
220+ @Test ( " #sliding window — carb outside lookahead never fires " )
221+ func slidingWindowNeverFires( ) {
222+ resetPending ( )
223+ let t0 = Date ( )
224+ let alarm = Alarm . futureCarbs ( threshold: 10 ) // 10-minute lookahead
225+ let carbDate = t0. addingTimeInterval ( 15 * 60 ) // 15 min in future
226+ let carbSample = CarbSample ( grams: 20 , date: carbDate)
227+
228+ // Tick at T+0: carb is 15 min away, outside 10-min window but tracked
229+ let data = AlarmData . withCarbs ( [ carbSample] )
230+ let r0 = cond. evaluate ( alarm: alarm, data: data, now: t0)
231+ #expect( !r0)
232+ #expect( Storage . shared. pendingFutureCarbs. value. count == 1 )
233+
234+ // Tick at T+5min: carb is now 10 min away (inside window), but
235+ // original distance was 15 min — must NOT fire
236+ let t1 = t0. addingTimeInterval ( 5 * 60 )
237+ let r1 = cond. evaluate ( alarm: alarm, data: data, now: t1)
238+ #expect( !r1)
239+
240+ // Tick at T+15min: carb is due — still must NOT fire
241+ let t2 = t0. addingTimeInterval ( 15 * 60 )
242+ let r2 = cond. evaluate ( alarm: alarm, data: data, now: t2)
243+ #expect( !r2)
244+ // Entry should be removed (due, outside original window)
245+ #expect( Storage . shared. pendingFutureCarbs. value. isEmpty)
246+ }
247+
248+ // MARK: - 12. Due entry outside original window removed without firing
249+
250+ @Test ( " #due entry outside original window removed without firing " )
251+ func dueOutsideWindowRemovedNoFire( ) {
252+ resetPending ( )
253+ let now = Date ( )
254+ let pastDate = now. addingTimeInterval ( - 60 ) // 1 min ago
255+
256+ // Entry was observed 20 min before its carb date (outside 10-min window)
257+ Storage . shared. pendingFutureCarbs. value = [
258+ PendingFutureCarb (
259+ carbDate: pastDate. timeIntervalSince1970,
260+ grams: 20 ,
261+ observedAt: pastDate. timeIntervalSince1970 - 20 * 60
262+ ) ,
263+ ]
264+
265+ let alarm = Alarm . futureCarbs ( threshold: 10 )
266+ let data = AlarmData . withCarbs ( [ CarbSample ( grams: 20 , date: pastDate) ] )
267+
268+ let result = cond. evaluate ( alarm: alarm, data: data, now: now)
269+
270+ #expect( !result)
271+ #expect( Storage . shared. pendingFutureCarbs. value. isEmpty)
272+ }
273+
274+ // MARK: - 13. Stale entry with existing carb is not evicted
275+
276+ @Test ( " #stale entry with existing carb is not evicted " )
277+ func staleWithExistingCarbNotEvicted( ) {
278+ resetPending ( )
279+ let now = Date ( )
280+ let futureDate = now. addingTimeInterval ( 300 ) // 5 min in the future
281+
282+ // Entry observed 3 hours ago, but carb still exists in recentCarbs
283+ Storage . shared. pendingFutureCarbs. value = [
284+ PendingFutureCarb (
285+ carbDate: futureDate. timeIntervalSince1970,
286+ grams: 20 ,
287+ observedAt: now. addingTimeInterval ( - 3 * 3600 ) . timeIntervalSince1970
288+ ) ,
289+ ]
290+
291+ let alarm = Alarm . futureCarbs ( )
292+ let data = AlarmData . withCarbs ( [ CarbSample ( grams: 20 , date: futureDate) ] )
293+
294+ let result = cond. evaluate ( alarm: alarm, data: data, now: now)
295+
296+ #expect( !result)
297+ // Entry must survive — carb still exists, don't evict
298+ #expect( Storage . shared. pendingFutureCarbs. value. count == 1 )
299+ }
215300}
0 commit comments