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.

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