@@ -41,27 +41,49 @@ vi.mock('../../_shared/supabaseClient.ts', () => ({
4141} ) ) ;
4242
4343vi . mock ( '../../_shared/metric-extraction.ts' , ( ) => ( {
44- extractDeepSleepPct : ( row : Record < string , unknown > ) => {
45- const vm = row . vendor_metadata as Record < string , unknown > | null ;
46- if ( ! vm ) return null ;
47- const deep = vm . deep_minutes ;
48- const total = row . total_duration_minutes as number ;
49- return deep != null && total > 0 ? ( ( deep as number ) / total ) * 100 : null ;
50- } ,
51- extractRemSleepPct : ( row : Record < string , unknown > ) => {
52- const vm = row . vendor_metadata as Record < string , unknown > | null ;
53- if ( ! vm ) return null ;
54- const rem = vm . rem_minutes ;
55- const total = row . total_duration_minutes as number ;
56- return rem != null && total > 0 ? ( ( rem as number ) / total ) * 100 : null ;
57- } ,
5844 extractHrvFromDailySummary : ( row : Record < string , unknown > ) => {
5945 const vm = row . vendor_metadata as Record < string , unknown > | null ;
6046 return vm ?. hrv_ms != null ? vm . hrv_ms as number : null ;
6147 } ,
62- extractHrvFromSleepSession : ( row : Record < string , unknown > ) => {
63- const vm = row . vendor_metadata as Record < string , unknown > | null ;
64- return vm ?. hrv_ms != null ? vm . hrv_ms as number : null ;
48+ } ) ) ;
49+
50+ // Mock daily-aggregation to avoid pulling in the full metric-extraction surface.
51+ // Provides a simplified aggregateSleepByDay that groups by wake date and computes
52+ // the metrics the starter-pack actually reads.
53+ vi . mock ( '../../_shared/daily-aggregation.ts' , ( ) => ( {
54+ aggregateSleepByDay : ( sessions : Array < Record < string , unknown > > ) => {
55+ const groups = new Map < string , Array < Record < string , unknown > > > ( ) ;
56+ for ( const s of sessions ) {
57+ const start = s . start_time as string ;
58+ const dur = s . total_duration_minutes as number ;
59+ if ( ! start || ! dur || dur <= 0 ) continue ;
60+ const wakeMs = new Date ( start ) . getTime ( ) + dur * 60 * 1000 ;
61+ const wakeDate = new Date ( wakeMs ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
62+ if ( ! groups . has ( wakeDate ) ) groups . set ( wakeDate , [ ] ) ;
63+ groups . get ( wakeDate ) ! . push ( s ) ;
64+ }
65+ const result = new Map < string , Map < string , number | null > > ( ) ;
66+ for ( const [ date , daySessions ] of groups ) {
67+ let totalMin = 0 ;
68+ let longest = daySessions [ 0 ] ;
69+ let longestDur = 0 ;
70+ for ( const s of daySessions ) {
71+ const d = ( s . total_duration_minutes as number ) ?? 0 ;
72+ totalMin += d ;
73+ if ( d > longestDur ) { longestDur = d ; longest = s ; }
74+ }
75+ const vm = longest . vendor_metadata as Record < string , unknown > | null ;
76+ const deep = vm ?. deep_minutes as number | undefined ;
77+ const rem = vm ?. rem_minutes as number | undefined ;
78+ const hrv = vm ?. hrv_ms as number | undefined ;
79+ const metrics = new Map < string , number | null > ( ) ;
80+ metrics . set ( 'sleep_duration' , totalMin > 0 ? totalMin / 60 : null ) ;
81+ metrics . set ( 'sleep_deep_pct' , deep != null && longestDur > 0 ? ( deep / longestDur ) * 100 : null ) ;
82+ metrics . set ( 'sleep_rem_pct' , rem != null && longestDur > 0 ? ( rem / longestDur ) * 100 : null ) ;
83+ metrics . set ( 'hrv_sleep' , hrv ?? null ) ;
84+ result . set ( date , metrics ) ;
85+ }
86+ return result ;
6587 } ,
6688} ) ) ;
6789
@@ -137,7 +159,9 @@ describe('StarterPack — R2: daily_history and days_of_data', () => {
137159 makeDailySummary ( '2026-03-03' , 61 , 7500 , 42 ) ,
138160 ] ;
139161 mockSleepSessions = [
162+ // start 03-01T22:00Z + 480min → wake 03-02T06:00Z → wake date: 03-02
140163 makeSleepSession ( '2026-03-01' , 480 , 90 , 100 ) ,
164+ // start 03-02T22:00Z + 460min → wake 03-03T05:40Z → wake date: 03-03
141165 makeSleepSession ( '2026-03-02' , 460 , 85 , 95 ) ,
142166 ] ;
143167
@@ -147,7 +171,8 @@ describe('StarterPack — R2: daily_history and days_of_data', () => {
147171 mockProvider ,
148172 ) ;
149173
150- // 3 unique dates from daily_summary + 2 from sleep (but 2026-03-01 and 2026-03-02 overlap)
174+ // daily_summary dates: 03-01, 03-02, 03-03
175+ // sleep wake dates: 03-02, 03-03 (overlap with daily_summary)
151176 // Unique dates: 2026-03-01, 2026-03-02, 2026-03-03
152177 expect ( result . days_of_data ) . toBe ( 3 ) ;
153178 } ) ;
@@ -158,6 +183,7 @@ describe('StarterPack — R2: daily_history and days_of_data', () => {
158183 makeDailySummary ( '2026-03-02' , 60 , 9000 , 48 ) ,
159184 ] ;
160185 mockSleepSessions = [
186+ // start 03-01T22:00Z + 480min → wake 03-02T06:00Z → wake date: 03-02
161187 makeSleepSession ( '2026-03-01' , 480 , 90 , 100 , 50 ) ,
162188 ] ;
163189
@@ -178,9 +204,9 @@ describe('StarterPack — R2: daily_history and days_of_data', () => {
178204 // steps should have 2 entries
179205 expect ( dh . steps ) . toHaveLength ( 2 ) ;
180206
181- // sleep_duration should have 1 entry (480 min = 8 hrs)
207+ // sleep_duration should have 1 entry (480 min = 8 hrs), attributed to wake date 03-02
182208 expect ( dh . sleep_duration ) . toHaveLength ( 1 ) ;
183- expect ( dh . sleep_duration [ 0 ] . value ) . toBe ( 8 ) ;
209+ expect ( dh . sleep_duration [ 0 ] ) . toEqual ( { date : '2026-03-02' , value : 8 } ) ;
184210
185211 // sleep_deep_pct should have 1 entry (90/480 * 100 = 18.75)
186212 expect ( dh . sleep_deep_pct ) . toHaveLength ( 1 ) ;
@@ -232,8 +258,34 @@ describe('StarterPack — R2: daily_history and days_of_data', () => {
232258 }
233259 } ) ;
234260
235- it ( 'sleep_sessions use date derived from start_time for daily_history' , async ( ) => {
261+ it ( 'sums nap + main sleep per day instead of averaging per session' , async ( ) => {
262+ mockSleepSessions = [
263+ // Main sleep: start 03-01T22:00Z + 480min → wake 03-02T06:00Z (8h)
264+ makeSleepSession ( '2026-03-01' , 480 , 90 , 100 ) ,
265+ // Nap same wake day: start 03-02T13:00 + 90min → wake 03-02T14:30 (1.5h)
266+ { start_time : '2026-03-02T13:00:00Z' , total_duration_minutes : 90 , vendor_metadata : { } } ,
267+ ] ;
268+
269+ const result = await getStarterPack (
270+ { connected_providers : [ 'whoop' ] } ,
271+ 'test-user' ,
272+ mockProvider ,
273+ ) ;
274+
275+ const dh = result . daily_history as Record < string , { date : string ; value : number } [ ] > ;
276+ // Both sessions wake on 03-02, so they should be summed: 8 + 1.5 = 9.5h
277+ expect ( dh . sleep_duration ) . toHaveLength ( 1 ) ;
278+ expect ( dh . sleep_duration [ 0 ] ) . toEqual ( { date : '2026-03-02' , value : 9.5 } ) ;
279+
280+ // The average should also be 9.5 (one day), not (8 + 1.5) / 2 = 4.75
281+ const scorecard = result . metric_scorecard as Array < { metric_key : string ; current_value : number } > ;
282+ const sleepMetric = scorecard . find ( ( m ) => m . metric_key === 'sleep_duration' ) ;
283+ expect ( sleepMetric ?. current_value ) . toBe ( 9.5 ) ;
284+ } ) ;
285+
286+ it ( 'sleep_sessions are attributed to wake date, not start date' , async ( ) => {
236287 mockSleepSessions = [
288+ // start 03-05T22:00Z + 480min → wake 03-06T06:00Z → wake date: 03-06
237289 makeSleepSession ( '2026-03-05' , 480 , 90 , 100 ) ,
238290 ] ;
239291
@@ -244,8 +296,7 @@ describe('StarterPack — R2: daily_history and days_of_data', () => {
244296 ) ;
245297
246298 const dh = result . daily_history as Record < string , { date : string ; value : number } [ ] > ;
247- // Sleep entries should have dates derived from start_time
248299 expect ( dh . sleep_duration ) . toHaveLength ( 1 ) ;
249- expect ( dh . sleep_duration [ 0 ] . date ) . toBe ( '2026-03-05 ' ) ;
300+ expect ( dh . sleep_duration [ 0 ] . date ) . toBe ( '2026-03-06 ' ) ;
250301 } ) ;
251302} ) ;
0 commit comments