Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

834 linesCodeBlameRaw
1use g1t_contracts::work::{
2 SignalAuthor, SignalDay, SignalHarness, SignalPull, SignalWeek, WaitingPull, WorkspaceSignalsArgs,
3};
4use rusqlite::types::ValueRef;
5use rusqlite::{Connection, ToSql, params};
6use serde::de::DeserializeOwned;
7
8use super::{AUTHORS, DAYS, HARNESSES, PULLS, PullRow, WAITING, WEEKS, WaitingRow, Window, is_person};
9
10const IDS: &str = r#"["a"]"#;
11
12/// D1 takes at most five terms in one compound SELECT; SQLite allows 500.
13const D1_COMPOUND_SELECT: i32 = 5;
14
15fn 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.
46fn 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
63fn 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.
72fn 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
94fn 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
106fn 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`.
112fn window(now: &str) -> Window {
113 super::window(&args("2026-10-11", 2, 7, "2026-09-01"), ms(now)).unwrap()
114}
115
116fn weeks(db: &Connection, now: &str) -> Vec<SignalWeek> {
117 rows(db, WEEKS, &[&IDS, &window(now).weeks])
118}
119
120fn in_week(found: &[SignalWeek], start: &str) -> SignalWeek {
121 found.iter().find(|week| week.start == start).cloned().unwrap()
122}
123
124fn 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
131fn 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]
139fn 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]
182fn 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]
249fn 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]
304fn 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]
337fn 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]
365fn 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]
395fn 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]
468fn 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]
518fn 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`.
530fn 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]
543fn 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]
673fn 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]
686fn 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]
719fn 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]
756fn 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]
803fn 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}