| 1 | use g1t_contracts::work::{ |
| 2 | SignalAuthor, SignalDay, SignalHarness, SignalPull, SignalWeek, WaitingPull, WorkspaceSignalsArgs, |
| 3 | }; |
| 4 | use rusqlite::types::ValueRef; |
| 5 | use rusqlite::{Connection, ToSql, params}; |
| 6 | use serde::de::DeserializeOwned; |
| 7 | |
| 8 | use super::{AUTHORS, DAYS, HARNESSES, PULLS, PullRow, WAITING, WEEKS, WaitingRow, Window, is_person}; |
| 9 | |
| 10 | const IDS: &str = r#"["a"]"#; |
| 11 | |
| 12 | /// D1 takes at most five terms in one compound SELECT; SQLite allows 500. |
| 13 | const D1_COMPOUND_SELECT: i32 = 5; |
| 14 | |
| 15 | fn schema() -> Connection { |
| 16 | let db = Connection::open_in_memory().unwrap(); |
| 17 | // Refuse what D1 refuses, so a statement these tests pass also runs there. |
| 18 | db.set_limit(rusqlite::limits::Limit::SQLITE_LIMIT_COMPOUND_SELECT, D1_COMPOUND_SELECT); |
| 19 | db.execute_batch( |
| 20 | "CREATE TABLE pulls (id TEXT, repo_id TEXT, number INTEGER, title TEXT DEFAULT 'A change', status TEXT, |
| 21 | item_id TEXT, milestone INTEGER, agent TEXT DEFAULT 'person', author_id TEXT DEFAULT 'usr_ana', |
| 22 | author_name TEXT DEFAULT 'ana', requested_by_id TEXT, requested_by_name TEXT, |
| 23 | source_branch TEXT, fork_repo_id TEXT, head_repo_id TEXT, head_commit TEXT, head_pushed_at TEXT, |
| 24 | created_at TEXT, updated_at TEXT, merged_at TEXT); |
| 25 | CREATE TABLE comments (repo_id TEXT, number INTEGER, author_id TEXT, agent_id TEXT, verdict TEXT, created_at TEXT, |
| 26 | kind TEXT NOT NULL DEFAULT 'comment'); |
| 27 | CREATE TABLE sessions (id TEXT, repo_id TEXT, pull INTEGER, started_at TEXT, parent_id TEXT, |
| 28 | actor_id TEXT DEFAULT 'usr_ana', actor_name TEXT DEFAULT 'ana', harness TEXT DEFAULT 'claude_code', |
| 29 | last_seen_at TEXT, cost_usd REAL); |
| 30 | CREATE TABLE session_entries (pull_id TEXT, seq INTEGER, at TEXT); |
| 31 | CREATE TABLE check_runs (id TEXT, pull_id TEXT, head_commit TEXT, created_at TEXT, finished_at TEXT); |
| 32 | CREATE TABLE commit_statuses (repo_id TEXT, sha TEXT, context TEXT, updated_at TEXT); |
| 33 | CREATE TABLE commit_check_runs (id TEXT, repo_id TEXT, head_sha TEXT, created_at TEXT, updated_at TEXT); |
| 34 | CREATE TABLE session_links (session_id TEXT, repo_id TEXT, kind TEXT, number INTEGER); |
| 35 | CREATE TABLE session_touches (session_id TEXT, repo_id TEXT, kind TEXT, value TEXT, at TEXT); |
| 36 | CREATE TABLE work_items (id TEXT, workspace TEXT, number INTEGER, title TEXT, state TEXT, |
| 37 | blocked_by TEXT DEFAULT '[]', milestone INTEGER); |
| 38 | CREATE TABLE milestones (workspace TEXT, number INTEGER, title TEXT, state TEXT, due_on TEXT, |
| 39 | created_at TEXT, closed_at TEXT);", |
| 40 | ) |
| 41 | .unwrap(); |
| 42 | db |
| 43 | } |
| 44 | |
| 45 | /// A pull request of repository `a`, by `ana` unless changed afterwards. |
| 46 | fn pull(db: &Connection, number: u32, status: &str, created: &str, merged: Option<&str>, updated: &str) { |
| 47 | db.execute( |
| 48 | "INSERT INTO pulls (id, repo_id, number, status, created_at, merged_at, updated_at, source_branch) |
| 49 | VALUES (?1, 'a', ?2, ?3, ?4, ?5, ?6, ?7)", |
| 50 | params![ |
| 51 | format!("pr_{number}"), |
| 52 | number, |
| 53 | status, |
| 54 | created, |
| 55 | merged, |
| 56 | updated, |
| 57 | format!("branch-{number}") |
| 58 | ], |
| 59 | ) |
| 60 | .unwrap(); |
| 61 | } |
| 62 | |
| 63 | fn comment(db: &Connection, number: u32, author: &str, agent: Option<&str>, verdict: Option<&str>, at: &str) { |
| 64 | db.execute( |
| 65 | "INSERT INTO comments (repo_id, number, author_id, agent_id, verdict, created_at) VALUES ('a', ?1, ?2, ?3, ?4, ?5)", |
| 66 | params![number, author, agent, verdict, at], |
| 67 | ) |
| 68 | .unwrap(); |
| 69 | } |
| 70 | |
| 71 | /// Every row `sql` returns, read as `T` the way D1's rows are. |
| 72 | fn rows<T: DeserializeOwned>(db: &Connection, sql: &str, binds: &[&dyn ToSql]) -> Vec<T> { |
| 73 | let mut statement = db.prepare(sql).unwrap(); |
| 74 | let names: Vec<String> = statement.column_names().into_iter().map(str::to_owned).collect(); |
| 75 | let mut found = statement.query(binds).unwrap(); |
| 76 | let mut out = Vec::new(); |
| 77 | while let Some(row) = found.next().unwrap() { |
| 78 | let mut object = serde_json::Map::new(); |
| 79 | for (i, name) in names.iter().enumerate() { |
| 80 | let value = match row.get_ref(i).unwrap() { |
| 81 | ValueRef::Null => serde_json::Value::Null, |
| 82 | ValueRef::Integer(n) => n.into(), |
| 83 | ValueRef::Real(n) => n.into(), |
| 84 | ValueRef::Text(text) => String::from_utf8_lossy(text).into_owned().into(), |
| 85 | ValueRef::Blob(_) => panic!("no blobs here"), |
| 86 | }; |
| 87 | object.insert(name.clone(), value); |
| 88 | } |
| 89 | out.push(serde_json::from_value(object.into()).unwrap()); |
| 90 | } |
| 91 | out |
| 92 | } |
| 93 | |
| 94 | fn args(until: &str, weeks: u32, days: u32, since: &str) -> WorkspaceSignalsArgs { |
| 95 | WorkspaceSignalsArgs { |
| 96 | workspace: "acme".into(), |
| 97 | repo_ids: vec!["a".into()], |
| 98 | viewer: None, |
| 99 | until: until.into(), |
| 100 | weeks, |
| 101 | days, |
| 102 | since: since.into(), |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | fn ms(at: &str) -> u64 { |
| 107 | g1t_contracts::time::parse_rfc3339(at).unwrap() |
| 108 | } |
| 109 | |
| 110 | /// Two weeks ending with Sunday 2026-10-11 (Mondays 09-28 and 10-05), |
| 111 | /// read on `now`. |
| 112 | fn window(now: &str) -> Window { |
| 113 | super::window(&args("2026-10-11", 2, 7, "2026-09-01"), ms(now)).unwrap() |
| 114 | } |
| 115 | |
| 116 | fn weeks(db: &Connection, now: &str) -> Vec<SignalWeek> { |
| 117 | rows(db, WEEKS, &[&IDS, &window(now).weeks]) |
| 118 | } |
| 119 | |
| 120 | fn in_week(found: &[SignalWeek], start: &str) -> SignalWeek { |
| 121 | found.iter().find(|week| week.start == start).cloned().unwrap() |
| 122 | } |
| 123 | |
| 124 | fn pulls(db: &Connection) -> Vec<SignalPull> { |
| 125 | rows::<PullRow>(db, PULLS, &[&IDS, &"acme", &"2026-01-01T00:00:00"]) |
| 126 | .into_iter() |
| 127 | .map(Into::into) |
| 128 | .collect() |
| 129 | } |
| 130 | |
| 131 | fn waiting(db: &Connection) -> Vec<WaitingPull> { |
| 132 | rows::<WaitingRow>(db, WAITING, &[&IDS, &"acme"]) |
| 133 | .into_iter() |
| 134 | .map(Into::into) |
| 135 | .collect() |
| 136 | } |
| 137 | |
| 138 | #[test] |
| 139 | fn checks_the_arguments() { |
| 140 | let now = ms("2026-10-18T00:00:00Z"); |
| 141 | let valid = |a: WorkspaceSignalsArgs| super::window(&a, now).is_some(); |
| 142 | assert!(valid(args("2026-10-11", 13, 28, "2025-09-06")), "400 days back"); |
| 143 | assert!(!valid(args("2026-10-11", 13, 28, "2025-09-05")), "401 days back"); |
| 144 | assert!(!valid(args("2026-10-11", 0, 7, "2026-10-01"))); |
| 145 | assert!(!valid(args("2026-10-11", 14, 7, "2026-10-01"))); |
| 146 | assert!(!valid(args("2026-10-11", 1, 0, "2026-10-01"))); |
| 147 | assert!(!valid(args("2026-10-11", 1, 29, "2026-10-01"))); |
| 148 | assert!(!valid(args("2026-10-11", 1, 7, "2026-10-12")), "since after until"); |
| 149 | assert!(valid(args("2026-10-11", 1, 7, "2026-10-11"))); |
| 150 | assert!(!valid(args("2026-02-30", 1, 7, "2026-02-01"))); |
| 151 | assert!(!valid(args("2026-10-11", 1, 7, "2026-10-1"))); |
| 152 | let mut many = args("2026-10-11", 1, 7, "2026-10-01"); |
| 153 | many.repo_ids = (0..101).map(|n| format!("r{n}")).collect(); |
| 154 | assert!(!valid(many)); |
| 155 | |
| 156 | let window = super::window(&args("2026-10-11", 2, 3, "2026-09-01"), now).unwrap(); |
| 157 | let weeks: serde_json::Value = serde_json::from_str(&window.weeks).unwrap(); |
| 158 | assert_eq!(weeks[0]["start"], "2026-09-28"); |
| 159 | assert_eq!(weeks[1]["start"], "2026-10-05"); |
| 160 | assert_eq!(weeks[1]["to"], "2026-10-12T00:00:00.000Z"); |
| 161 | assert_eq!( |
| 162 | weeks[1]["cut"], "2026-10-12T00:00:00.000Z", |
| 163 | "a week already over ends at its end" |
| 164 | ); |
| 165 | assert_eq!( |
| 166 | ( |
| 167 | window.days_from.as_str(), |
| 168 | window.end.as_str(), |
| 169 | window.since.as_str(), |
| 170 | window.recent_from.as_str() |
| 171 | ), |
| 172 | ( |
| 173 | "2026-10-09T00:00:00", |
| 174 | "2026-10-12T00:00:00", |
| 175 | "2026-09-01T00:00:00", |
| 176 | "2026-07-14T00:00:00" |
| 177 | ) |
| 178 | ); |
| 179 | } |
| 180 | |
| 181 | #[test] |
| 182 | fn an_agents_pull_request_is_one_a_session_is_tied_to() { |
| 183 | let db = schema(); |
| 184 | for number in 1..=6 { |
| 185 | pull( |
| 186 | &db, |
| 187 | number, |
| 188 | "open", |
| 189 | "2026-10-06T12:00:00Z", |
| 190 | None, |
| 191 | "2026-10-06T12:00:00Z", |
| 192 | ); |
| 193 | } |
| 194 | db.execute_batch( |
| 195 | "UPDATE pulls SET agent = 'agent' WHERE number = 1; |
| 196 | UPDATE pulls SET author_id = 'usr_g1t_agent', requested_by_id = 'usr_ana', requested_by_name = 'ana' WHERE number = 2; |
| 197 | -- 3 by a link, 4 by the session saying so, 5 by a push to its branch. |
| 198 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_3', 'a', NULL, '2026-10-06T11:00:00Z'); |
| 199 | INSERT INTO session_links VALUES ('ses_3', 'a', 'pull', 3); |
| 200 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_4', 'a', 4, '2026-10-06T13:00:00Z'); |
| 201 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_5', 'a', NULL, '2026-10-06T11:30:00Z'); |
| 202 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_5b', 'a', NULL, '2026-10-06T10:00:00.500Z'); |
| 203 | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_5', 'a', 'branch', 'branch-5'); |
| 204 | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_5b', 'a', 'branch', 'branch-5'); |
| 205 | -- 6's branch is on a fork: a push to the same name is not its. |
| 206 | UPDATE pulls SET fork_repo_id = 'fork' WHERE number = 6; |
| 207 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_6', 'a', NULL, '2026-10-06T11:00:00Z'); |
| 208 | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_6', 'a', 'branch', 'branch-6'); |
| 209 | -- A session of another repository with the same number is not its. |
| 210 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_x', 'b', 6, '2026-10-06T11:00:00Z');", |
| 211 | ) |
| 212 | .unwrap(); |
| 213 | pull(&db, 7, "open", "2026-10-06T12:00:00Z", None, "2026-10-06T12:00:00Z"); |
| 214 | let mut agents: Vec<(u32, bool)> = pulls(&db).iter().map(|p| (p.number, p.agent)).collect(); |
| 215 | agents.sort(); |
| 216 | assert_eq!( |
| 217 | agents, |
| 218 | [ |
| 219 | (1, true), |
| 220 | (2, true), |
| 221 | (3, true), |
| 222 | (4, true), |
| 223 | (5, true), |
| 224 | (6, false), |
| 225 | (7, false) |
| 226 | ] |
| 227 | ); |
| 228 | assert_eq!( |
| 229 | pulls(&db).iter().find(|p| p.number == 2).unwrap().owner_id, |
| 230 | "usr_ana", |
| 231 | "whose g1t's is" |
| 232 | ); |
| 233 | |
| 234 | let mut drafts: Vec<(u32, Option<f64>)> = waiting(&db).iter().map(|p| (p.number, p.draft_seconds)).collect(); |
| 235 | drafts.sort_by_key(|(number, _)| *number); |
| 236 | // 4's session started after it opened: no draft time. 5's first |
| 237 | // session started 2 hours less half a second before. |
| 238 | assert!(drafts[2].0 == 3 && (drafts[2].1.unwrap() - 3600.0).abs() < 0.01); |
| 239 | assert_eq!(drafts[3], (4, None)); |
| 240 | assert!(drafts[4].0 == 5 && (drafts[4].1.unwrap() - 7199.5).abs() < 0.01); |
| 241 | assert_eq!((drafts[0].1, drafts[5].1), (None, None)); |
| 242 | |
| 243 | let week = in_week(&weeks(&db, "2026-10-18T00:00:00Z"), "2026-10-05"); |
| 244 | assert_eq!(week.agent_drafted, 2); |
| 245 | assert!((week.agent_draft_seconds - 10799.5).abs() < 0.01); |
| 246 | } |
| 247 | |
| 248 | #[test] |
| 249 | fn rework_is_a_persons_request_for_changes_before_it_merged() { |
| 250 | let db = schema(); |
| 251 | pull( |
| 252 | &db, |
| 253 | 1, |
| 254 | "merged", |
| 255 | "2026-10-05T00:00:00Z", |
| 256 | Some("2026-10-07T00:00:00Z"), |
| 257 | "2026-10-07T00:00:00Z", |
| 258 | ); |
| 259 | pull( |
| 260 | &db, |
| 261 | 2, |
| 262 | "merged", |
| 263 | "2026-10-05T00:00:00Z", |
| 264 | Some("2026-10-07T00:00:00Z"), |
| 265 | "2026-10-07T00:00:00Z", |
| 266 | ); |
| 267 | db.execute_batch("UPDATE pulls SET agent = 'agent'").unwrap(); |
| 268 | // None of these is a person's, before it merged, on 1. |
| 269 | comment(&db, 1, "usr_ana", None, Some("request_changes"), "2026-10-06T00:00:00Z"); |
| 270 | comment( |
| 271 | &db, |
| 272 | 1, |
| 273 | "usr_g1t_agent", |
| 274 | None, |
| 275 | Some("request_changes"), |
| 276 | "2026-10-06T00:00:00Z", |
| 277 | ); |
| 278 | comment( |
| 279 | &db, |
| 280 | 1, |
| 281 | "agt_margo", |
| 282 | Some("agt_margo"), |
| 283 | Some("request_changes"), |
| 284 | "2026-10-06T00:00:00Z", |
| 285 | ); |
| 286 | comment(&db, 1, "usr_bo", None, Some("approve"), "2026-10-06T00:00:00Z"); |
| 287 | comment(&db, 1, "usr_bo", None, Some("request_changes"), "2026-10-08T00:00:00Z"); |
| 288 | comment(&db, 2, "usr_bo", None, Some("request_changes"), "2026-10-06T00:00:00Z"); |
| 289 | let found = weeks(&db, "2026-10-18T00:00:00Z"); |
| 290 | assert_eq!(found.len(), 1); |
| 291 | let week = &found[0]; |
| 292 | assert_eq!( |
| 293 | (week.start.as_str(), week.merged, week.agent_merged, week.agent_reworked), |
| 294 | ("2026-10-05", 2, 2, 1) |
| 295 | ); |
| 296 | // A person's pull request asked for changes is not an agent's rework. |
| 297 | db.execute("UPDATE pulls SET agent = 'person' WHERE number = 2", []) |
| 298 | .unwrap(); |
| 299 | let week = in_week(&weeks(&db, "2026-10-18T00:00:00Z"), "2026-10-05"); |
| 300 | assert_eq!((week.merged, week.agent_merged, week.agent_reworked), (2, 1, 0)); |
| 301 | } |
| 302 | |
| 303 | #[test] |
| 304 | fn a_first_review_is_a_persons_other_than_the_owner() { |
| 305 | let db = schema(); |
| 306 | pull(&db, 1, "open", "2026-10-06T00:00:00Z", None, "2026-10-06T00:00:00Z"); |
| 307 | db.execute_batch( |
| 308 | "UPDATE pulls SET author_id = 'usr_g1t_agent', requested_by_id = 'owner', requested_by_name = 'owner'", |
| 309 | ) |
| 310 | .unwrap(); |
| 311 | comment(&db, 1, "usr_g1t_agent", None, Some("approve"), "2026-10-06T01:00:00Z"); |
| 312 | comment(&db, 1, "owner", None, Some("approve"), "2026-10-06T02:00:00Z"); |
| 313 | comment( |
| 314 | &db, |
| 315 | 1, |
| 316 | "agt_margo", |
| 317 | Some("agt_margo"), |
| 318 | Some("approve"), |
| 319 | "2026-10-06T03:00:00Z", |
| 320 | ); |
| 321 | comment(&db, 1, "usr_bo", None, None, "2026-10-06T03:30:00Z"); |
| 322 | assert!(weeks(&db, "2026-10-18T00:00:00Z").iter().all(|w| w.reviewed == 0)); |
| 323 | assert_eq!( |
| 324 | waiting(&db).len(), |
| 325 | 1, |
| 326 | "g1t, an agent, the owner and a plain comment are no review" |
| 327 | ); |
| 328 | comment(&db, 1, "usr_bo", None, Some("request_changes"), "2026-10-06T04:00:00Z"); |
| 329 | comment(&db, 1, "usr_cy", None, Some("approve"), "2026-10-06T05:00:00Z"); |
| 330 | let week = in_week(&weeks(&db, "2026-10-18T00:00:00Z"), "2026-10-05"); |
| 331 | assert_eq!(week.reviewed, 1); |
| 332 | assert!((week.review_seconds - 4.0 * 3600.0).abs() < 1.0); |
| 333 | assert!(waiting(&db).is_empty()); |
| 334 | } |
| 335 | |
| 336 | #[test] |
| 337 | fn waiting_pull_requests_name_the_work_they_block() { |
| 338 | let db = schema(); |
| 339 | pull(&db, 10, "open", "2026-10-02T00:00:00Z", None, "2026-10-02T00:00:00Z"); |
| 340 | pull(&db, 11, "open", "2026-10-01T00:00:00Z", None, "2026-10-01T00:00:00Z"); |
| 341 | pull(&db, 12, "draft", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); |
| 342 | db.execute_batch( |
| 343 | "UPDATE pulls SET item_id = 'wki_1' WHERE number = 10; |
| 344 | INSERT INTO work_items VALUES ('wki_1', 'acme', 1, 'Login', 'open', '[]', 3); |
| 345 | INSERT INTO work_items VALUES ('wki_2', 'acme', 2, 'Signup', 'open', '[\"wki_9\", \"wki_1\"]', NULL); |
| 346 | INSERT INTO work_items VALUES ('wki_4', 'acme', 4, 'Billing', 'open', '[\"wki_1\"]', NULL); |
| 347 | INSERT INTO work_items VALUES ('wki_5', 'acme', 5, 'Done', 'closed', '[\"wki_1\"]', NULL); |
| 348 | INSERT INTO work_items VALUES ('wki_6', 'other', 6, 'Elsewhere', 'open', '[\"wki_1\"]', NULL); |
| 349 | INSERT INTO work_items VALUES ('wki_7', 'acme', 7, 'Unrelated', 'open', '[\"wki_2\"]', NULL);", |
| 350 | ) |
| 351 | .unwrap(); |
| 352 | let found = waiting(&db); |
| 353 | let shown: Vec<(u32, Option<u32>, Vec<u32>)> = found.iter().map(|p| (p.number, p.item, p.blocks.clone())).collect(); |
| 354 | assert_eq!( |
| 355 | shown, |
| 356 | [(11, None, vec![]), (10, Some(1), vec![2, 4])], |
| 357 | "oldest first; drafts wait on no one" |
| 358 | ); |
| 359 | // Its milestone is its work item's. |
| 360 | let pull = pulls(&db).into_iter().find(|p| p.number == 10).unwrap(); |
| 361 | assert_eq!((pull.item, pull.milestone), (Some(1), Some(3))); |
| 362 | } |
| 363 | |
| 364 | #[test] |
| 365 | fn a_pull_requests_own_milestone_counts_only_when_it_is_the_workspaces() { |
| 366 | let db = schema(); |
| 367 | pull(&db, 1, "open", "2026-10-02T00:00:00Z", None, "2026-10-02T00:00:00Z"); |
| 368 | pull(&db, 2, "open", "2026-10-02T00:00:00Z", None, "2026-10-02T00:00:00Z"); |
| 369 | pull( |
| 370 | &db, |
| 371 | 3, |
| 372 | "merged", |
| 373 | "2025-01-01T00:00:00Z", |
| 374 | Some("2026-10-03T00:00:00Z"), |
| 375 | "2026-10-03T00:00:00Z", |
| 376 | ); |
| 377 | pull(&db, 4, "closed", "2025-01-01T00:00:00Z", None, "2026-10-03T00:00:00Z"); |
| 378 | db.execute_batch( |
| 379 | "UPDATE pulls SET milestone = 1 WHERE number = 1; |
| 380 | UPDATE pulls SET milestone = 8 WHERE number = 2; |
| 381 | INSERT INTO milestones VALUES ('acme', 1, 'Launch', 'open', NULL, '2026-01-01T00:00:00Z', NULL); |
| 382 | INSERT INTO milestones VALUES ('other', 8, 'Not ours', 'open', NULL, '2026-01-01T00:00:00Z', NULL);", |
| 383 | ) |
| 384 | .unwrap(); |
| 385 | let mut found: Vec<(u32, Option<u32>)> = pulls(&db).iter().map(|p| (p.number, p.milestone)).collect(); |
| 386 | found.sort(); |
| 387 | assert_eq!( |
| 388 | found, |
| 389 | [(1, Some(1)), (2, None), (3, None)], |
| 390 | "merged since counts; closed long-opened does not" |
| 391 | ); |
| 392 | } |
| 393 | |
| 394 | #[test] |
| 395 | fn authors_are_people_with_merged_work() { |
| 396 | let db = schema(); |
| 397 | pull( |
| 398 | &db, |
| 399 | 1, |
| 400 | "merged", |
| 401 | "2025-01-01T00:00:00Z", |
| 402 | Some("2025-01-02T00:00:00Z"), |
| 403 | "2025-01-02T00:00:00Z", |
| 404 | ); |
| 405 | pull( |
| 406 | &db, |
| 407 | 2, |
| 408 | "merged", |
| 409 | "2026-10-01T00:00:00Z", |
| 410 | Some("2026-10-02T00:00:00Z"), |
| 411 | "2026-10-02T00:00:00Z", |
| 412 | ); |
| 413 | pull(&db, 3, "open", "2026-10-01T00:00:00Z", None, "2026-10-01T00:00:00Z"); |
| 414 | for (number, author) in [ |
| 415 | (4, "usr_g1t_agent"), |
| 416 | (5, "agt_margo"), |
| 417 | (6, "wsp_acme"), |
| 418 | (7, "g1t"), |
| 419 | (8, "svc_runner"), |
| 420 | ] { |
| 421 | pull( |
| 422 | &db, |
| 423 | number, |
| 424 | "merged", |
| 425 | "2026-10-01T00:00:00Z", |
| 426 | Some("2026-10-02T00:00:00Z"), |
| 427 | "2026-10-02T00:00:00Z", |
| 428 | ); |
| 429 | db.execute( |
| 430 | "UPDATE pulls SET author_id = ?1 WHERE number = ?2", |
| 431 | params![author, number], |
| 432 | ) |
| 433 | .unwrap(); |
| 434 | } |
| 435 | // g1t's agent's work for a person is theirs, under their name now. |
| 436 | pull( |
| 437 | &db, |
| 438 | 9, |
| 439 | "merged", |
| 440 | "2026-10-01T00:00:00Z", |
| 441 | Some("2026-10-03T00:00:00Z"), |
| 442 | "2026-10-03T00:00:00Z", |
| 443 | ); |
| 444 | db.execute_batch( |
| 445 | "UPDATE pulls SET author_id = 'usr_g1t_agent', requested_by_id = 'usr_ana', requested_by_name = 'ana-renamed' WHERE number = 9", |
| 446 | ) |
| 447 | .unwrap(); |
| 448 | let found: Vec<SignalAuthor> = |
| 449 | rows::<SignalAuthor>(&db, AUTHORS, &[&IDS, &"2026-07-14T00:00:00", &"2026-10-12T00:00:00"]) |
| 450 | .into_iter() |
| 451 | .filter(|author| is_person(&author.owner_id)) |
| 452 | .collect(); |
| 453 | assert_eq!(found.len(), 1); |
| 454 | let ana = &found[0]; |
| 455 | assert_eq!( |
| 456 | ( |
| 457 | ana.owner_id.as_str(), |
| 458 | ana.owner_name.as_str(), |
| 459 | ana.first_merged_at.as_str(), |
| 460 | ana.last_merged_at.as_str() |
| 461 | ), |
| 462 | ("usr_ana", "ana-renamed", "2025-01-02T00:00:00Z", "2026-10-03T00:00:00Z") |
| 463 | ); |
| 464 | assert_eq!((ana.merged, ana.recent), (3, 2)); |
| 465 | } |
| 466 | |
| 467 | #[test] |
| 468 | fn days_count_what_happened_on_each() { |
| 469 | let db = schema(); |
| 470 | pull( |
| 471 | &db, |
| 472 | 1, |
| 473 | "merged", |
| 474 | "2026-10-09T10:00:00Z", |
| 475 | Some("2026-10-10T10:00:00Z"), |
| 476 | "2026-10-10T10:00:00Z", |
| 477 | ); |
| 478 | pull(&db, 2, "open", "2026-10-08T10:00:00Z", None, "2026-10-08T10:00:00Z"); |
| 479 | db.execute_batch( |
| 480 | "UPDATE pulls SET agent = 'agent' WHERE number = 1; |
| 481 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_1', 'a', NULL, '2026-10-10T09:00:00Z'); |
| 482 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_2', 'b', NULL, '2026-10-10T09:00:00Z');", |
| 483 | ) |
| 484 | .unwrap(); |
| 485 | comment(&db, 1, "usr_bo", None, Some("request_changes"), "2026-10-10T01:00:00Z"); |
| 486 | comment(&db, 1, "usr_bo", None, None, "2026-10-10T02:00:00Z"); |
| 487 | comment(&db, 1, "usr_ana", None, None, "2026-10-10T03:00:00Z"); |
| 488 | comment(&db, 1, "g1t", None, None, "2026-10-10T03:00:00Z"); |
| 489 | comment(&db, 1, "agt_margo", Some("agt_margo"), None, "2026-10-10T03:00:00Z"); |
| 490 | let window = window("2026-10-18T00:00:00Z"); |
| 491 | let days: Vec<SignalDay> = rows(&db, DAYS, &[&IDS, &window.days_from, &window.end]); |
| 492 | let shown: Vec<(&str, u32, u32, u32, u32, u32, u32)> = days |
| 493 | .iter() |
| 494 | .map(|d| { |
| 495 | ( |
| 496 | d.day.as_str(), |
| 497 | d.opened, |
| 498 | d.merged, |
| 499 | d.agent_merged, |
| 500 | d.reworked, |
| 501 | d.reviews, |
| 502 | d.sessions, |
| 503 | ) |
| 504 | }) |
| 505 | .collect(); |
| 506 | assert_eq!( |
| 507 | shown, |
| 508 | [ |
| 509 | ("2026-10-08", 1, 0, 0, 0, 0, 0), |
| 510 | ("2026-10-09", 1, 0, 0, 0, 0, 0), |
| 511 | ("2026-10-10", 0, 1, 1, 1, 2, 1) |
| 512 | ], |
| 513 | "the owner, g1t and an agent are not reviewers; another repository's session is not counted" |
| 514 | ); |
| 515 | } |
| 516 | |
| 517 | #[test] |
| 518 | fn days_leave_out_g1t_as_its_system_ids_say() { |
| 519 | let mut ids = vec![g1t_contracts::system::ID]; |
| 520 | ids.extend(g1t_contracts::system::LEGACY_IDS); |
| 521 | for id in ids { |
| 522 | assert!(DAYS.contains(&format!("'{id}'")), "{id}"); |
| 523 | assert!(!is_person(id)); |
| 524 | } |
| 525 | assert!(is_person("usr_ana")); |
| 526 | } |
| 527 | |
| 528 | /// Whether pull request 1 (open since 09-01, head `abc`) is stalled in the |
| 529 | /// week of 10-05, read on `now`, after `setup`. |
| 530 | fn stalled_after(now: &str, setup: &str) -> u32 { |
| 531 | let db = schema(); |
| 532 | pull(&db, 1, "open", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); |
| 533 | db.execute_batch("UPDATE pulls SET head_commit = 'abc'").unwrap(); |
| 534 | db.execute_batch(setup).unwrap(); |
| 535 | // A week with nothing in it is left out. |
| 536 | weeks(&db, now) |
| 537 | .iter() |
| 538 | .find(|week| week.start == "2026-10-05") |
| 539 | .map_or(0, |week| week.stalled) |
| 540 | } |
| 541 | |
| 542 | #[test] |
| 543 | fn stalled_is_open_with_nothing_in_the_three_days_before_the_cut() { |
| 544 | // The week of 10-05 is over on 10-18: its cut is 10-12T00:00, and the |
| 545 | // three days before it start 10-09T00:00. |
| 546 | let at = |setup: &str| stalled_after("2026-10-18T00:00:00Z", setup); |
| 547 | assert_eq!(at(""), 1, "nothing at all"); |
| 548 | let comment = |kind: &str, when: &str| { |
| 549 | format!( |
| 550 | "INSERT INTO comments (repo_id, number, author_id, kind, created_at) VALUES ('a', 1, 'usr_bo', '{kind}', '{when}')" |
| 551 | ) |
| 552 | }; |
| 553 | assert_eq!(at(&comment("comment", "2026-10-10T00:00:00Z")), 0, "a comment"); |
| 554 | assert_eq!( |
| 555 | at(&comment("event", "2026-10-09T00:00:00Z")), |
| 556 | 0, |
| 557 | "an event exactly three days before" |
| 558 | ); |
| 559 | assert_eq!( |
| 560 | at(&comment("comment", "2026-10-08T23:59:59Z")), |
| 561 | 1, |
| 562 | "only before the three days" |
| 563 | ); |
| 564 | assert_eq!(at(&comment("comment", "2026-10-12T00:00:00Z")), 1, "only at the cut"); |
| 565 | assert_eq!( |
| 566 | at( |
| 567 | "INSERT INTO comments (repo_id, number, author_id, created_at) VALUES ('a', 2, 'usr_bo', '2026-10-10T00:00:00Z')" |
| 568 | ), |
| 569 | 1, |
| 570 | "another pull request's comment" |
| 571 | ); |
| 572 | assert_eq!( |
| 573 | at("UPDATE pulls SET head_pushed_at = '2026-10-11T00:00:00Z'"), |
| 574 | 0, |
| 575 | "a push" |
| 576 | ); |
| 577 | assert_eq!( |
| 578 | at("UPDATE pulls SET head_pushed_at = '2026-10-01T00:00:00Z'"), |
| 579 | 1, |
| 580 | "an old push" |
| 581 | ); |
| 582 | assert_eq!( |
| 583 | at("INSERT INTO session_entries VALUES ('pr_1', 1, '2026-10-10T00:00:00Z')"), |
| 584 | 0, |
| 585 | "a session entry" |
| 586 | ); |
| 587 | assert_eq!( |
| 588 | at("INSERT INTO check_runs VALUES ('chk', 'pr_1', 'abc', '2026-10-05T00:00:00Z', '2026-10-10T00:00:00Z')"), |
| 589 | 0, |
| 590 | "acceptance checks running then" |
| 591 | ); |
| 592 | assert_eq!( |
| 593 | at("INSERT INTO commit_statuses VALUES ('a', 'abc', 'CI', '2026-10-10T00:00:00Z')"), |
| 594 | 0, |
| 595 | "a status on its head" |
| 596 | ); |
| 597 | assert_eq!( |
| 598 | at("INSERT INTO commit_statuses VALUES ('a', 'old', 'CI', '2026-10-10T00:00:00Z')"), |
| 599 | 1, |
| 600 | "a status on another commit" |
| 601 | ); |
| 602 | assert_eq!( |
| 603 | at("INSERT INTO commit_check_runs VALUES ('ck', 'a', 'abc', '2026-10-08T00:00:00Z', '2026-10-09T06:00:00Z')"), |
| 604 | 0, |
| 605 | "a check run on its head" |
| 606 | ); |
| 607 | let session = "INSERT INTO sessions (id, repo_id, pull, started_at, last_seen_at) |
| 608 | VALUES ('ses_1', 'a', 1, '2026-10-01T00:00:00Z',"; |
| 609 | assert_eq!( |
| 610 | at(&format!("{session} '2026-10-09T12:00:00Z')")), |
| 611 | 0, |
| 612 | "a session running then" |
| 613 | ); |
| 614 | assert_eq!( |
| 615 | at(&format!("{session} '2026-10-08T00:00:00Z')")), |
| 616 | 1, |
| 617 | "a session over before" |
| 618 | ); |
| 619 | assert_eq!( |
| 620 | at(&format!( |
| 621 | "{session} '2026-10-01T00:00:00Z'); |
| 622 | INSERT INTO sessions (id, repo_id, parent_id, started_at, last_seen_at) |
| 623 | VALUES ('ses_2', 'a', 'ses_1', '2026-10-09T00:00:00Z', '2026-10-10T00:00:00Z');" |
| 624 | )), |
| 625 | 0, |
| 626 | "its session's subagent running then" |
| 627 | ); |
| 628 | |
| 629 | // Opened at least three days before the cut. |
| 630 | assert_eq!( |
| 631 | at("UPDATE pulls SET created_at = '2026-10-09T00:00:00Z'"), |
| 632 | 1, |
| 633 | "exactly three days" |
| 634 | ); |
| 635 | assert_eq!( |
| 636 | at("UPDATE pulls SET created_at = '2026-10-09T00:00:01Z'"), |
| 637 | 0, |
| 638 | "a second less" |
| 639 | ); |
| 640 | // Open at the cut; drafts count. |
| 641 | assert_eq!(at("UPDATE pulls SET status = 'draft'"), 1, "a draft"); |
| 642 | assert_eq!( |
| 643 | at("UPDATE pulls SET status = 'merged', merged_at = '2026-10-11T00:00:00Z'"), |
| 644 | 0, |
| 645 | "merged before the cut" |
| 646 | ); |
| 647 | assert_eq!( |
| 648 | at("UPDATE pulls SET status = 'merged', merged_at = '2026-10-12T00:00:00Z'"), |
| 649 | 1, |
| 650 | "merged at the cut" |
| 651 | ); |
| 652 | assert_eq!( |
| 653 | at("UPDATE pulls SET status = 'closed', updated_at = '2026-10-11T00:00:00Z'"), |
| 654 | 0, |
| 655 | "closed before" |
| 656 | ); |
| 657 | assert_eq!( |
| 658 | at("UPDATE pulls SET status = 'closed', updated_at = '2026-10-13T00:00:00Z'"), |
| 659 | 1, |
| 660 | "closed after" |
| 661 | ); |
| 662 | |
| 663 | // Read mid-week, the cut is now, 10-08T12:00: the three days before it |
| 664 | // start 10-05T12:00. |
| 665 | let mid = |setup: &str| stalled_after("2026-10-08T12:00:00Z", setup); |
| 666 | assert_eq!(mid(&comment("comment", "2026-10-05T12:00:00Z")), 0); |
| 667 | assert_eq!(mid(&comment("comment", "2026-10-05T11:59:59Z")), 1); |
| 668 | assert_eq!(mid("UPDATE pulls SET created_at = '2026-10-05T12:00:00Z'"), 1); |
| 669 | assert_eq!(mid("UPDATE pulls SET created_at = '2026-10-05T12:00:01Z'"), 0); |
| 670 | } |
| 671 | |
| 672 | #[test] |
| 673 | fn stalled_counts_each_week_at_its_own_cut() { |
| 674 | let db = schema(); |
| 675 | pull(&db, 1, "open", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); |
| 676 | pull(&db, 2, "open", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); |
| 677 | // 1 was busy before the first week's cut (10-05), 2 before the second's. |
| 678 | comment(&db, 1, "usr_bo", None, None, "2026-10-04T00:00:00Z"); |
| 679 | comment(&db, 2, "usr_bo", None, None, "2026-10-11T00:00:00Z"); |
| 680 | let found = weeks(&db, "2026-10-18T00:00:00Z"); |
| 681 | let stalled: Vec<(&str, u32)> = found.iter().map(|w| (w.start.as_str(), w.stalled)).collect(); |
| 682 | assert_eq!(stalled, [("2026-09-28", 1), ("2026-10-05", 1)]); |
| 683 | } |
| 684 | |
| 685 | #[test] |
| 686 | fn a_weeks_session_cost_is_what_its_sessions_cost() { |
| 687 | let db = schema(); |
| 688 | db.execute_batch( |
| 689 | "INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_1', 'a', '2026-10-06T00:00:00.000Z', 1.5); |
| 690 | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) |
| 691 | VALUES ('ses_2', 'a', 'ses_1', '2026-10-07T00:00:00.000Z', 0.25); |
| 692 | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_3', 'a', '2026-10-08T00:00:00.000Z', NULL); |
| 693 | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_4', 'a', '2026-10-04T23:59:59.999Z', 2); |
| 694 | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_5', 'a', '2026-10-05T00:00:00.000Z', 0.5); |
| 695 | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_6', 'a', '2026-10-12T00:00:00.000Z', 7); |
| 696 | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_7', 'b', '2026-10-06T00:00:00.000Z', 100);", |
| 697 | ) |
| 698 | .unwrap(); |
| 699 | let found = weeks(&db, "2026-10-18T00:00:00Z"); |
| 700 | let costs: Vec<(&str, f64, u32)> = found |
| 701 | .iter() |
| 702 | .map(|w| (w.start.as_str(), w.session_cost_usd, w.merged)) |
| 703 | .collect(); |
| 704 | // No pull requests at all: the weeks show for their cost alone. A |
| 705 | // subagent's counts; one of no known cost counts as nothing. |
| 706 | assert_eq!(costs, [("2026-09-28", 2.0, 0), ("2026-10-05", 2.25, 0)]); |
| 707 | |
| 708 | // A week whose sessions cost nothing known is left out. |
| 709 | db.execute_batch("UPDATE sessions SET cost_usd = NULL WHERE id = 'ses_4'") |
| 710 | .unwrap(); |
| 711 | let found = weeks(&db, "2026-10-18T00:00:00Z"); |
| 712 | assert_eq!( |
| 713 | found.iter().map(|w| w.start.as_str()).collect::<Vec<_>>(), |
| 714 | ["2026-10-05"] |
| 715 | ); |
| 716 | } |
| 717 | |
| 718 | #[test] |
| 719 | fn a_pull_requests_session_cost_is_its_share() { |
| 720 | let db = schema(); |
| 721 | for number in 1..=4 { |
| 722 | pull( |
| 723 | &db, |
| 724 | number, |
| 725 | "open", |
| 726 | "2026-10-06T00:00:00Z", |
| 727 | None, |
| 728 | "2026-10-06T00:00:00Z", |
| 729 | ); |
| 730 | } |
| 731 | db.execute_batch( |
| 732 | "-- A root session tied to 1 by saying so and to 2 by a link. |
| 733 | INSERT INTO sessions (id, repo_id, pull, started_at, cost_usd) VALUES ('ses_r', 'a', 1, '2026-10-06T00:00:00Z', 4); |
| 734 | INSERT INTO session_links VALUES ('ses_r', 'a', 'pull', 2); |
| 735 | -- Its subagent tied to nothing counts with it; so does that one's. |
| 736 | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) |
| 737 | VALUES ('ses_a', 'a', 'ses_r', '2026-10-06T00:00:00Z', 1); |
| 738 | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) |
| 739 | VALUES ('ses_c', 'a', 'ses_a', '2026-10-06T00:00:00Z', 2); |
| 740 | -- A subagent tied itself, by a push to 3's branch, counts with that. |
| 741 | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) |
| 742 | VALUES ('ses_b', 'a', 'ses_r', '2026-10-06T00:00:00Z', 3); |
| 743 | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_b', 'a', 'branch', 'branch-3'); |
| 744 | -- A session whose cost is not known. |
| 745 | INSERT INTO sessions (id, repo_id, pull, started_at, cost_usd) VALUES ('ses_n', 'a', 3, '2026-10-06T00:00:00Z', NULL); |
| 746 | -- A session tied to nothing counts with nothing. |
| 747 | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_x', 'a', '2026-10-06T00:00:00Z', 9);", |
| 748 | ) |
| 749 | .unwrap(); |
| 750 | let mut costs: Vec<(u32, f64)> = pulls(&db).iter().map(|p| (p.number, p.session_cost_usd)).collect(); |
| 751 | costs.sort_by_key(|(number, _)| *number); |
| 752 | assert_eq!(costs, [(1, 3.5), (2, 3.5), (3, 3.0), (4, 0.0)]); |
| 753 | } |
| 754 | |
| 755 | #[test] |
| 756 | fn harnesses_are_peoples_root_sessions() { |
| 757 | let db = schema(); |
| 758 | db.execute_batch( |
| 759 | "INSERT INTO sessions (id, repo_id, started_at, harness, actor_name) |
| 760 | VALUES ('s1', 'a', '2026-01-01T00:00:00Z', 'claude_code', 'ana'); |
| 761 | INSERT INTO sessions (id, repo_id, started_at, harness, actor_name) |
| 762 | VALUES ('s2', 'a', '2026-05-01T00:00:00Z', 'claude_code', 'ana-new'); |
| 763 | INSERT INTO sessions (id, repo_id, started_at, harness, actor_name) |
| 764 | VALUES ('s3', 'a', '2026-03-01T00:00:00Z', 'codex', 'ana'); |
| 765 | -- A subagent is its parent's: not counted, and not when they first used it. |
| 766 | INSERT INTO sessions (id, repo_id, parent_id, started_at, harness) |
| 767 | VALUES ('s4', 'a', 's1', '2025-12-01T00:00:00Z', 'claude_code'); |
| 768 | INSERT INTO sessions (id, repo_id, parent_id, started_at, harness) |
| 769 | VALUES ('s5', 'a', 's1', '2025-12-01T00:00:00Z', 'cursor'); |
| 770 | -- Robots, and another repository. |
| 771 | INSERT INTO sessions (id, repo_id, started_at, actor_id) VALUES ('s6', 'a', '2026-01-01T00:00:00Z', 'usr_g1t_agent'); |
| 772 | INSERT INTO sessions (id, repo_id, started_at, actor_id) VALUES ('s7', 'a', '2026-01-01T00:00:00Z', 'agt_margo'); |
| 773 | INSERT INTO sessions (id, repo_id, started_at) VALUES ('s8', 'b', '2025-01-01T00:00:00Z');", |
| 774 | ) |
| 775 | .unwrap(); |
| 776 | let found: Vec<SignalHarness> = rows::<SignalHarness>(&db, HARNESSES, &[&IDS]) |
| 777 | .into_iter() |
| 778 | .filter(|harness| is_person(&harness.owner_id)) |
| 779 | .collect(); |
| 780 | let shown: Vec<(&str, &str, &str, &str, &str, u32)> = found |
| 781 | .iter() |
| 782 | .map(|h| { |
| 783 | ( |
| 784 | h.repo_id.as_str(), |
| 785 | h.owner_id.as_str(), |
| 786 | h.owner_name.as_str(), |
| 787 | h.harness.as_str(), |
| 788 | h.first_at.as_str(), |
| 789 | h.sessions, |
| 790 | ) |
| 791 | }) |
| 792 | .collect(); |
| 793 | assert_eq!( |
| 794 | shown, |
| 795 | [ |
| 796 | ("a", "usr_ana", "ana-new", "claude_code", "2026-01-01T00:00:00Z", 2), |
| 797 | ("a", "usr_ana", "ana-new", "codex", "2026-03-01T00:00:00Z", 1) |
| 798 | ] |
| 799 | ); |
| 800 | } |
| 801 | |
| 802 | #[test] |
| 803 | fn agent_commits_are_a_sessions_own_outside_any_pull_request() { |
| 804 | let db = schema(); |
| 805 | pull(&db, 1, "open", "2026-10-06T00:00:00Z", None, "2026-10-06T00:00:00Z"); |
| 806 | db.execute_batch( |
| 807 | "UPDATE pulls SET source_branch = 'feature' WHERE number = 1; |
| 808 | INSERT INTO sessions (id, repo_id, started_at) VALUES ('ses_main', 'a', '2026-10-06T00:00:00Z'); |
| 809 | INSERT INTO sessions (id, repo_id, started_at) VALUES ('ses_pr', 'a', '2026-10-06T00:00:00Z'); |
| 810 | INSERT INTO session_touches VALUES ('ses_main', 'a', 'commit', 'c1', '2026-10-06T10:00:00Z'); |
| 811 | INSERT INTO session_touches VALUES ('ses_main', 'a', 'commit', 'c1', '2026-10-06T10:00:01Z'); |
| 812 | INSERT INTO session_touches VALUES ('ses_main', 'a', 'commit', 'c2', '2026-10-01T10:00:00Z'); |
| 813 | INSERT INTO session_touches VALUES ('ses_main', 'b', 'commit', 'c9', '2026-10-06T10:00:00Z'); |
| 814 | INSERT INTO session_touches VALUES ('ses_pr', 'a', 'branch', 'feature', '2026-10-06T09:00:00Z'); |
| 815 | INSERT INTO session_touches VALUES ('ses_pr', 'a', 'commit', 'c3', '2026-10-06T10:00:00Z');", |
| 816 | ) |
| 817 | .unwrap(); |
| 818 | let found = weeks(&db, "2026-10-18T00:00:00Z"); |
| 819 | // c1 once, in its week; c2 in the week before; c3 belongs to a pull request's session; c9 to another repository. |
| 820 | assert_eq!(in_week(&found, "2026-10-05").agent_commits, 1); |
| 821 | assert_eq!(in_week(&found, "2026-09-28").agent_commits, 1); |
| 822 | let window = window("2026-10-18T00:00:00Z"); |
| 823 | let days: Vec<SignalDay> = rows(&db, DAYS, &[&IDS, &window.days_from, &window.end]); |
| 824 | let commits: Vec<(&str, u32)> = days |
| 825 | .iter() |
| 826 | .filter(|d| d.agent_commits > 0) |
| 827 | .map(|d| (d.day.as_str(), d.agent_commits)) |
| 828 | .collect(); |
| 829 | assert_eq!( |
| 830 | commits, |
| 831 | [("2026-10-06", 1)], |
| 832 | "the days read are the last two weeks; c2 is before them" |
| 833 | ); |
| 834 | } |