@@ -17,11 +17,13 @@ pub(crate) fn metric_key_for_name<'a>(
1717pub ( crate ) fn session_from_signpost ( db : & mut SqliteConnection , metric : & str ) -> Result < Session > {
1818 use crate :: schema:: metrics:: dsl:: * ;
1919 let metric_key = metric_key_for_name ( db, metric) ?;
20- let query = metrics
21- . order ( timestamp. asc ( ) )
20+ // Use the most recent signpost occurrence as the session start
21+ let start_query = metrics
22+ . order ( timestamp. desc ( ) )
2223 . filter ( metric_key_id. eq ( metric_key. id ) )
2324 . limit ( 1 ) ;
24- let start = query. first :: < Metric > ( db) ?;
25+ let start = start_query. first :: < Metric > ( db) ?;
26+ // End is the latest metric in the DB (approximates NOW)
2527 let end_query = metrics. order ( timestamp. desc ( ) ) . limit ( 1 ) ;
2628 let end = end_query. first :: < Metric > ( db) ?;
2729 if end. timestamp <= start. timestamp {
@@ -87,3 +89,106 @@ pub(crate) fn metrics_summary_for_signpost_and_keys(
8789 results. insert ( "session.duration" . to_string ( ) , duration_secs) ;
8890 Ok ( results)
8991}
92+
93+ #[ cfg( test) ]
94+ mod tests {
95+ use super :: * ;
96+ use crate :: models:: { MetricKey , NewMetric } ;
97+
98+ /// Create a temp DB and return the connection + temp dir (keep alive for DB lifetime)
99+ fn setup_test_db ( ) -> ( SqliteConnection , tempfile:: TempDir ) {
100+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
101+ let db_path = dir. path ( ) . join ( "test.db" ) ;
102+ let db = crate :: setup_db ( & db_path) . unwrap ( ) ;
103+ ( db, dir)
104+ }
105+
106+ /// Insert a metric key, returning its id
107+ fn insert_key ( db : & mut SqliteConnection , name : & str ) -> i64 {
108+ let key = MetricKey :: key_by_name ( name, db) . unwrap ( ) ;
109+ key. id
110+ }
111+
112+ /// Insert a metric row
113+ fn insert_metric ( db : & mut SqliteConnection , key_id : i64 , ts : f64 , val : f64 ) {
114+ use crate :: schema:: metrics:: dsl:: * ;
115+ diesel:: insert_into ( metrics)
116+ . values ( & NewMetric {
117+ timestamp : ts,
118+ metric_key_id : key_id,
119+ value : val,
120+ } )
121+ . execute ( db)
122+ . unwrap ( ) ;
123+ }
124+
125+ /// Sets up a DB simulating stale signpost data from a previous session:
126+ /// - "app.started" signpost at t=100.0 (old/stale session)
127+ /// - "app.started" signpost at t=500.0 (current session)
128+ /// - "cpu" metrics at t=500, 510, 520 with values 50, 60, 70 (current session)
129+ /// - "cpu" metrics at t=100, 110 with values 10, 20 (old session)
130+ /// - latest metric in DB is at t=520
131+ fn populate_test_db ( db : & mut SqliteConnection ) {
132+ let signpost_id = insert_key ( db, "app.started" ) ;
133+ let cpu_id = insert_key ( db, "cpu" ) ;
134+
135+ // Old session data (stale)
136+ insert_metric ( db, signpost_id, 100.0 , 1.0 ) ;
137+ insert_metric ( db, cpu_id, 100.0 , 10.0 ) ;
138+ insert_metric ( db, cpu_id, 110.0 , 20.0 ) ;
139+
140+ // Current session data
141+ insert_metric ( db, signpost_id, 500.0 , 1.0 ) ;
142+ insert_metric ( db, cpu_id, 500.0 , 50.0 ) ;
143+ insert_metric ( db, cpu_id, 510.0 , 60.0 ) ;
144+ insert_metric ( db, cpu_id, 520.0 , 70.0 ) ;
145+ }
146+
147+ #[ test]
148+ fn test_session_from_signpost_uses_latest_signpost ( ) {
149+ // With stale signpost data at t=100 and current at t=500,
150+ // start should be 500 (latest signpost), not 100 (oldest).
151+ let ( mut db, _dir) = setup_test_db ( ) ;
152+ populate_test_db ( & mut db) ;
153+
154+ let session = session_from_signpost ( & mut db, "app.started" ) . unwrap ( ) ;
155+ assert_eq ! (
156+ session. start_time, 500.0 ,
157+ "start should be the most recent signpost (t=500), not the stale one (t=100)"
158+ ) ;
159+ assert_eq ! ( session. end_time, 520.0 ) ;
160+ }
161+
162+ #[ test]
163+ fn test_summary_duration_not_inflated_by_stale_signpost ( ) {
164+ // Duration should be based on the latest signpost, not a stale one from a previous session.
165+ let ( mut db, _dir) = setup_test_db ( ) ;
166+ populate_test_db ( & mut db) ;
167+
168+ let results =
169+ metrics_summary_for_signpost_and_keys ( & mut db, "app.started" , vec ! [ "cpu" . to_string( ) ] )
170+ . unwrap ( ) ;
171+
172+ // cpu average across t=500..520 (current session only): (50+60+70)/3 = 60
173+ assert ! (
174+ ( results[ "cpu" ] - 60.0 ) . abs( ) < f64 :: EPSILON ,
175+ "cpu average should only include current session metrics"
176+ ) ;
177+ // session.duration should be 520 - 500 = 20, not 520 - 100 = 420
178+ assert_eq ! (
179+ results[ "session.duration" ] , 20.0 ,
180+ "duration should be 20s (t=500..520), not inflated to 420s by stale signpost"
181+ ) ;
182+ }
183+
184+ #[ test]
185+ fn test_average_for_session_with_explicit_range ( ) {
186+ let ( mut db, _dir) = setup_test_db ( ) ;
187+ populate_test_db ( & mut db) ;
188+
189+ // With a manually bounded session t=500..510, avg = (50+60)/2 = 55
190+ let session = Session :: new ( 500.0 , 510.0 ) ;
191+ let avg = average_for_session ( & mut db, "cpu" , & session) . unwrap ( ) ;
192+ assert ! ( ( avg - 55.0 ) . abs( ) < f64 :: EPSILON ) ;
193+ }
194+ }
0 commit comments