From 400f805061157306300a85d49a50020fe6ee23d4 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:28:12 +0700 Subject: [PATCH] Keep Codex cost fixtures on the local day Codex session fixtures in the cost scanner tests stamped their events at now - 1h but filed the session under today's local date folder. In the first hour after local midnight (00:00-01:00Z on the UTC CircleCI runner) that hour fell on yesterday, so the tests read the wrong day bucket, and the fork fixtures landed in two different date folders, which flipped the folder scan order. CircleCI builds 977, 978 and 979 failed this way: codex_source_recovery_keeps_appended_duplicate_unpriced_after_cache_reload, paginated_continuation_raises_inherited_baseline_from_total_last and paginated_history_base_equal_parent_keeps_true_fork_subtraction. A shared helper now returns now - 1h, or the start of the local day when that hour reaches back into yesterday, and the fixtures take both the event time and the date folder from it. A unit test pins the helper at fixed times around midnight in UTC and UTC+7. Production code is unchanged. --- rust/src/cost_scanner/tests.rs | 66 ++++++++++++++++++++---- rust/src/cost_scanner/tests/paginated.rs | 4 +- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/rust/src/cost_scanner/tests.rs b/rust/src/cost_scanner/tests.rs index 3b16c92a1c..b1cc1373b7 100644 --- a/rust/src/cost_scanner/tests.rs +++ b/rust/src/cost_scanner/tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::core::{CodexSessionLineage, CostUsagePricing}; +use chrono::{FixedOffset, NaiveTime, TimeZone}; use std::io::Write; #[test] @@ -671,17 +672,59 @@ fn claude_scan_counts_final_incomplete_jsonl_line() { let _removed = std::fs::remove_file(&path); } +/// An event time for a fresh Codex session fixture: an hour ago, kept on today's local date. +/// +/// The scanner files each event under its local date, and the session fixtures live in today's +/// date folder. A plain `now - 1h` lands on yesterday in the first hour after local midnight +/// (00:00-01:00Z on the UTC CI runner), so tests that read today's bucket or rely on the day +/// folder scan order failed in that hour. +fn recent_codex_fixture_time() -> DateTime { + recent_fixture_time_at(Local::now()) +} + +/// `now - 1h`, or the start of `now`'s local day when that hour reaches back into yesterday. +fn recent_fixture_time_at(now: DateTime) -> DateTime { + let hour_ago = now.clone() - Duration::hours(1); + if hour_ago.date_naive() == now.date_naive() { + return hour_ago.with_timezone(&Utc); + } + now.timezone() + .from_local_datetime(&now.date_naive().and_time(NaiveTime::MIN)) + .earliest() + .unwrap_or(now) + .with_timezone(&Utc) +} + +#[test] +fn recent_codex_fixture_time_stays_on_the_local_day() { + let utc_plus_7 = FixedOffset::east_opt(7 * 3600).unwrap(); + let at = |hour, minute| { + utc_plus_7 + .with_ymd_and_hms(2026, 10, 1, hour, minute, 0) + .unwrap() + }; + assert_eq!(recent_fixture_time_at(at(8, 30)), at(7, 30)); + assert_eq!(recent_fixture_time_at(at(1, 0)), at(0, 0)); + assert_eq!(recent_fixture_time_at(at(0, 40)), at(0, 0)); + assert_eq!(recent_fixture_time_at(at(0, 0)), at(0, 0)); + + let ci_run = Utc.with_ymd_and_hms(2026, 10, 1, 0, 45, 0).unwrap(); + assert_eq!( + recent_fixture_time_at(ci_run), + Utc.with_ymd_and_hms(2026, 10, 1, 0, 0, 0).unwrap() + ); +} + fn write_codex_session_fixture(sessions_root: &Path, name: &str, input_tokens: u64) -> PathBuf { - let today = Local::now().date_naive(); + let event_time = recent_codex_fixture_time(); + let today = event_time.with_timezone(&Local).date_naive(); let day_dir = sessions_root .join(today.format("%Y").to_string()) .join(today.format("%m").to_string()) .join(today.format("%d").to_string()); std::fs::create_dir_all(&day_dir).unwrap(); let path = day_dir.join(name); - let ts = (Utc::now() - Duration::hours(1)) - .format("%Y-%m-%dT%H:%M:%S%.3fZ") - .to_string(); + let ts = event_time.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); let body = format!( r#"{{"timestamp":"{ts}","type":"event_msg","payload":{{"type":"token_count","info":{{"model":"gpt-5","total_token_usage":{{"input_tokens":{input_tokens},"cached_input_tokens":0,"output_tokens":5}}}}}}}} "# @@ -695,13 +738,13 @@ fn write_codex_session_fixture_with_inputs( name: &str, input_tokens: &[u64], ) -> PathBuf { - let today = Local::now().date_naive(); + let base = recent_codex_fixture_time(); + let today = base.with_timezone(&Local).date_naive(); let day_dir = sessions_root .join(today.format("%Y").to_string()) .join(today.format("%m").to_string()) .join(today.format("%d").to_string()); std::fs::create_dir_all(&day_dir).unwrap(); - let base = Utc::now() - Duration::hours(1); let mut body = String::new(); for (index, input) in input_tokens.iter().enumerate() { let timestamp = (base @@ -1565,9 +1608,9 @@ fn codex_source_recovery_keeps_appended_duplicate_unpriced_after_cache_reload() first_cache.last_scan_unix_ms = 1; JsonlScanner::save_cache(ProviderId::Codex, &mut first_cache, Some(&cache_root)); - let timestamp = (Utc::now() - Duration::minutes(30)) - .format("%Y-%m-%dT%H:%M:%S%.3fZ") - .to_string(); + // Half an hour after the fixture's row, so both rows share one local day. + let appended_time = recent_codex_fixture_time() + Duration::minutes(30); + let timestamp = appended_time.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); let appended = format!( r#"{{"timestamp":"{timestamp}","type":"event_msg","payload":{{"type":"token_count","info":{{"model":"gpt-5","total_token_usage":{{"input_tokens":200,"cached_input_tokens":0,"output_tokens":10}}}}}}}}"# ) + "\n"; @@ -1580,7 +1623,10 @@ fn codex_source_recovery_keeps_appended_duplicate_unpriced_after_cache_reload() let (_, _, second_cache) = scanner.scan_codex_detailed_with_cache(None); let usage = second_cache.files.get(&path_key).expect("file cache"); - let day = Local::now().format("%Y-%m-%d").to_string(); + let day = appended_time + .with_timezone(&Local) + .format("%Y-%m-%d") + .to_string(); assert_eq!(usage.days[&day]["gpt-5-priority"], vec![100, 0, 5]); assert_eq!( usage.days[&day][CostUsagePricing::CODEX_UNATTRIBUTED_MODEL], diff --git a/rust/src/cost_scanner/tests/paginated.rs b/rust/src/cost_scanner/tests/paginated.rs index 592d484823..1853bf4ab4 100644 --- a/rust/src/cost_scanner/tests/paginated.rs +++ b/rust/src/cost_scanner/tests/paginated.rs @@ -140,7 +140,7 @@ fn paginated_continuation_raises_inherited_baseline_from_total_last() { let root = tempfile::tempdir().unwrap(); let sessions = root.path().join("sessions"); let cache_root = root.path().join("cache"); - let base = Utc::now() - Duration::hours(1); + let base = recent_codex_fixture_time(); write_codex_fork_session_fixture( &sessions, "ancestor.jsonl", @@ -201,7 +201,7 @@ fn paginated_history_base_equal_parent_keeps_true_fork_subtraction() { let root = tempfile::tempdir().unwrap(); let sessions = root.path().join("sessions"); let cache_root = root.path().join("cache"); - let base = Utc::now() - Duration::hours(1); + let base = recent_codex_fixture_time(); write_codex_fork_session_fixture( &sessions, "parent.jsonl",