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.

351 linesCodeBlameRaw
1//! Issues moving into work items, against the schema the migrations leave:
2//! what migrations/0049_work_items.sql moves, what the job then numbers and
3//! merges (work_migration.rs), how `#N` is read (items.rs `REF_SQL`), what
4//! landing does (closing.rs) and where new numbers come from.
5
6use rusqlite::{Connection, OptionalExtension, params};
7
8use crate::closing::{LAND_SQL, PART_SQL};
9use crate::items::{NEXT_NUMBER, Named, REF_SQL, REMEMBER_REPO, RefRow};
10use crate::migrated::{database, database_before, migrate_from};
11use crate::work_migration::{RECORD_REPO, TRANSFERRED, finish_sql};
12
13const MIGRATION: &str = "0049_work_items.sql";
14
15/// Binds `sql` with the first of `values` it names (`?1` up to its
16/// highest), as D1 is bound.
17fn run(db: &Connection, sql: &str, values: &[&dyn rusqlite::ToSql]) -> usize {
18 db.execute(sql, &values[..g1t_kit::rename::parameters(sql)]).unwrap()
19}
20
21fn strings(db: &Connection, sql: &str) -> Vec<String> {
22 let mut statement = db.prepare(sql).unwrap();
23 statement.query_map([], |row| row.get::<_, String>(0)).unwrap().map(Result::unwrap).collect()
24}
25
26fn numbers(db: &Connection, sql: &str) -> Vec<(String, Option<u32>)> {
27 let mut statement = db.prepare(sql).unwrap();
28 statement.query_map([], |row| Ok((row.get(0)?, row.get(1)?))).unwrap().map(Result::unwrap).collect()
29}
30
31/// Two repositories of `acme` whose issue and pull request numbers collide
32/// (web: issues 1, 3 and 5, pull requests 2 and 4; api: issues 1 and 2,
33/// pull request 3), and one of `globex`, as they were before work items.
34fn legacy() -> Connection {
35 let db = database_before(MIGRATION);
36 db.execute_batch(
37 "INSERT INTO counters (repo_id, last) VALUES ('rep_web', 5), ('rep_api', 3), ('rep_site', 2);
38 INSERT INTO issues (id, repo_id, number, title, body, labels, state, reason, resolved_by, blocked_by, milestone,
39 author_id, author_name, created_at, updated_at, closed_at)
40 VALUES ('iss_web1', 'rep_web', 1, 'Header', '', '[\"bug\"]', 'open', NULL, NULL, '[]', NULL, 'usr_1', 'ana', '2026-01-01', '2026-01-01', NULL),
41 ('iss_api1', 'rep_api', 1, 'Rate limits', '', '[]', 'open', NULL, NULL, '[]', NULL, 'usr_1', 'ana', '2026-01-02', '2026-01-02', NULL),
42 ('iss_web3', 'rep_web', 3, 'Footer', '', '[]', 'closed', 'completed', 4, '[1]', NULL, 'usr_1', 'ana', '2026-01-03', '2026-01-06', '2026-01-06'),
43 ('iss_api2', 'rep_api', 2, 'Tokens', '', '[]', 'open', NULL, NULL, '[1]', 1, 'usr_2', 'bo', '2026-01-04', '2026-01-04', NULL),
44 ('iss_web5', 'rep_web', 5, 'Logo', '', '[\"docs\"]', 'open', NULL, NULL, '[1, 3]', 1, 'usr_2', 'bo', '2026-01-05', '2026-01-05', NULL),
45 ('iss_site1', 'rep_site', 1, 'Site', '', '[]', 'open', NULL, NULL, '[]', NULL, 'usr_1', 'ana', '2026-01-01', '2026-01-01', NULL);
46 INSERT INTO pulls (id, repo_id, number, issue_id, issue_number, title, agent, runtime, status, closes, milestone,
47 author_id, author_name, created_at, updated_at)
48 VALUES ('pr_web2', 'rep_web', 2, NULL, NULL, 'Tidy', 'ana', 'external', 'open', '[]', NULL, 'usr_1', 'ana', '', ''),
49 ('pr_web4', 'rep_web', 4, 'iss_web3', 3, 'Fix footer', 'ana', 'external', 'merged', '[3, 1]', 1, 'usr_1', 'ana', '', ''),
50 ('pr_api3', 'rep_api', 3, NULL, NULL, 'Limits', 'ana', 'external', 'open', '[]', NULL, 'usr_1', 'ana', '', ''),
51 ('pr_site2', 'rep_site', 2, NULL, NULL, 'Site', 'ana', 'external', 'open', '[]', NULL, 'usr_1', 'ana', '', '');
52 INSERT INTO comments (id, repo_id, number, author_id, author_name, body, created_at)
53 VALUES ('cmt_issue', 'rep_web', 3, 'usr_1', 'ana', 'On the footer', ''),
54 ('cmt_pull', 'rep_web', 4, 'usr_1', 'ana', 'On the fix', '');
55 INSERT INTO sessions (id, repo_id, issue, actor_id, actor_name, harness, started_at, last_seen_at)
56 VALUES ('ses_1', 'rep_api', 2, 'usr_1', 'ana', 'codex', '', '');
57 INSERT INTO claims (id, repo_id, issue, actor_id, actor_name, ttl_minutes, created_at, expires_at)
58 VALUES ('clm_1', 'rep_api', 2, 'usr_1', 'ana', 30, '', '');
59 INSERT INTO labels (repo_id, name, color, description, created_at)
60 VALUES ('rep_web', 'bug', 'd73a4a', 'Broken', '2026-01-01'), ('rep_web', 'docs', '0075ca', 'Words', '2026-01-03'),
61 ('rep_api', 'bug', '0e8a16', 'Something broke', '2025-12-01');
62 INSERT INTO milestones (repo_id, number, title, created_at, updated_at)
63 VALUES ('rep_web', 1, 'Launch', '2026-01-02', '2026-01-02'), ('rep_api', 1, 'Beta', '2026-01-01', '2026-01-01');",
64 )
65 .unwrap();
66 migrate_from(&db, MIGRATION);
67 db
68}
69
70/// What the job records of each repository, then its last step.
71fn finish(db: &Connection) {
72 for (id, workspace, path) in [("rep_web", "acme", "acme/web"), ("rep_api", "acme", "acme/api"), ("rep_site", "globex", "globex/site")] {
73 for sql in RECORD_REPO {
74 run(db, sql, &[&id, &workspace, &path, &0]);
75 }
76 }
77 for sql in finish_sql() {
78 run(db, &sql, &[&"2026-10-17T00:00:00Z"]);
79 }
80}
81
82fn reference(db: &Connection, repo: Option<&str>, workspace: &str, number: u32) -> Option<Named> {
83 let row: RefRow = db
84 .query_row(REF_SQL, params![repo, workspace, number], |row| {
85 Ok(RefRow { pull_here: row.get(0)?, legacy: row.get(1)?, item: row.get(2)?, pull_there: row.get(3)? })
86 })
87 .unwrap();
88 row.named(repo.is_some())
89}
90
91#[test]
92fn the_migration_moves_every_issue_and_what_named_it_by_number() {
93 let db = legacy();
94 // Every issue is a work item, not yet in a workspace, filed from its
95 // repository under its old number.
96 assert_eq!(numbers(&db, "SELECT id, number FROM work_items ORDER BY id").len(), 6);
97 assert!(strings(&db, "SELECT id FROM work_items WHERE workspace IS NOT NULL OR number IS NOT NULL").is_empty());
98 let origins: Vec<(String, Option<u32>)> =
99 numbers(&db, "SELECT item_id, legacy_number FROM work_item_repos WHERE role = 'origin' ORDER BY item_id");
100 assert_eq!(origins.len(), 6);
101 assert!(origins.contains(&("iss_web3".to_owned(), Some(3))) && origins.contains(&("iss_api1".to_owned(), Some(1))));
102 // Blockers by id, in the same repository: web's #1 and #3, never api's #1.
103 assert_eq!(strings(&db, "SELECT blocked_by FROM work_items WHERE id = 'iss_web5'"), [r#"["iss_web1","iss_web3"]"#]);
104 assert_eq!(strings(&db, "SELECT blocked_by FROM work_items WHERE id = 'iss_api2'"), [r#"["iss_api1"]"#]);
105 // What resolved it, and that it landed where it was filed.
106 assert_eq!(
107 strings(&db, "SELECT resolutions FROM work_items WHERE id = 'iss_web3'"),
108 [r#"[{"repo_id":"rep_web","pull":4,"commit":null,"pushed_by":null}]"#]
109 );
110 assert_eq!(strings(&db, "SELECT landed_at FROM work_item_repos WHERE item_id = 'iss_web3'"), ["2026-01-06"]);
111 // A pull request names its work item and what it closes by id.
112 assert_eq!(strings(&db, "SELECT item_id FROM pulls WHERE id = 'pr_web4'"), ["iss_web3"]);
113 assert_eq!(strings(&db, "SELECT closes_items FROM pulls WHERE id = 'pr_web4'"), [r#"["iss_web3","iss_web1"]"#]);
114 // An issue's comment is the work item's; a pull request's stays its own.
115 assert_eq!(strings(&db, "SELECT item_id FROM comments WHERE id = 'cmt_issue' AND repo_id IS NULL"), ["iss_web3"]);
116 assert_eq!(strings(&db, "SELECT repo_id FROM comments WHERE id = 'cmt_pull' AND item_id IS NULL"), ["rep_web"]);
117 // Sessions and claims follow the issue by id; the claim keeps its checkout.
118 assert_eq!(strings(&db, "SELECT item_id FROM sessions WHERE id = 'ses_1'"), ["iss_api2"]);
119 assert_eq!(strings(&db, "SELECT item_id || ' ' || repo_id FROM claims WHERE id = 'clm_1'"), ["iss_api2 rep_api"]);
120 // Nothing points at a work item that is not there.
121 assert!(db.prepare("PRAGMA foreign_key_check").unwrap().query_map([], |_| Ok(())).unwrap().next().is_none());
122 // Until the job runs, an old #N is read by its old number.
123 assert_eq!(reference(&db, Some("rep_web"), "acme", 3), Some(Named::Item("iss_web3".into())));
124 assert_eq!(reference(&db, Some("rep_web"), "acme", 4), Some(Named::PullHere));
125 assert_eq!(reference(&db, Some("rep_api"), "acme", 1), Some(Named::Item("iss_api1".into())));
126}
127
128#[test]
129fn the_job_numbers_each_workspace_after_everything_it_used_oldest_first() {
130 let db = legacy();
131 finish(&db);
132 // acme's repositories used up to 5: its work items go on from 6, in
133 // the order they were opened, whichever repository they came from.
134 assert_eq!(
135 numbers(&db, "SELECT id, number FROM work_items WHERE workspace = 'acme' ORDER BY number"),
136 [
137 ("iss_web1".to_owned(), Some(6)),
138 ("iss_api1".to_owned(), Some(7)),
139 ("iss_web3".to_owned(), Some(8)),
140 ("iss_api2".to_owned(), Some(9)),
141 ("iss_web5".to_owned(), Some(10)),
142 ]
143 );
144 assert_eq!(numbers(&db, "SELECT id, number FROM work_items WHERE workspace = 'globex'"), [("iss_site1".to_owned(), Some(3))]);
145 assert_eq!(numbers(&db, "SELECT workspace, last FROM work_counters ORDER BY workspace"), [
146 ("acme".to_owned(), Some(10)),
147 ("globex".to_owned(), Some(3)),
148 ]);
149 // The per-repository counters are emptied; the job is done, and running
150 // its last step again changes nothing.
151 assert!(strings(&db, "SELECT repo_id FROM counters").is_empty());
152 assert_eq!(strings(&db, "SELECT done_at FROM work_jobs WHERE name = 'work_items'"), ["2026-10-17T00:00:00Z"]);
153 finish(&db);
154 assert_eq!(numbers(&db, "SELECT id, number FROM work_items WHERE id = 'iss_web5'"), [("iss_web5".to_owned(), Some(10))]);
155 assert_eq!(numbers(&db, "SELECT workspace, last FROM work_counters WHERE workspace = 'acme'"), [("acme".to_owned(), Some(10))]);
156 // A pull request's own number and its paths follow.
157 assert_eq!(strings(&db, "SELECT path FROM work_item_repos WHERE item_id = 'iss_api2'"), ["acme/api"]);
158}
159
160#[test]
161fn labels_merge_by_name_and_the_first_made_keeps_its_color() {
162 let db = legacy();
163 finish(&db);
164 let labels: Vec<(String, String, String)> = {
165 let mut statement = db.prepare("SELECT name, color, description FROM labels WHERE workspace = 'acme' ORDER BY name").unwrap();
166 statement.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))).unwrap().map(Result::unwrap).collect()
167 };
168 assert_eq!(
169 labels,
170 [
171 ("bug".to_owned(), "0e8a16".to_owned(), "Something broke".to_owned()),
172 ("docs".to_owned(), "0075ca".to_owned(), "Words".to_owned()),
173 ]
174 );
175 assert!(strings(&db, "SELECT name FROM repo_labels").is_empty());
176}
177
178#[test]
179fn milestones_are_renumbered_in_the_workspace_and_what_was_in_them_follows() {
180 let db = legacy();
181 finish(&db);
182 // Beta (api's, made first) is 1 and Launch (web's) 2 in acme.
183 let milestones: Vec<(u32, String, String)> = {
184 let mut statement =
185 db.prepare("SELECT number, title, origin_repo_id FROM milestones WHERE workspace = 'acme' ORDER BY number").unwrap();
186 statement.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))).unwrap().map(Result::unwrap).collect()
187 };
188 assert_eq!(milestones, [(1, "Beta".to_owned(), "rep_api".to_owned()), (2, "Launch".to_owned(), "rep_web".to_owned())]);
189 // web's #5 was in web's milestone 1, api's #2 in api's: Launch and Beta.
190 assert_eq!(
191 numbers(&db, "SELECT id, milestone FROM work_items WHERE milestone IS NOT NULL ORDER BY id"),
192 [("iss_api2".to_owned(), Some(1)), ("iss_web5".to_owned(), Some(2))]
193 );
194 assert_eq!(numbers(&db, "SELECT id, milestone FROM pulls WHERE id = 'pr_web4'"), [("pr_web4".to_owned(), Some(2))]);
195 // Run again, nothing moves twice.
196 finish(&db);
197 assert_eq!(numbers(&db, "SELECT id, milestone FROM pulls WHERE id = 'pr_web4'"), [("pr_web4".to_owned(), Some(2))]);
198 assert_eq!(strings(&db, "SELECT title FROM milestones WHERE workspace = 'acme'").len(), 2);
199}
200
201#[test]
202fn a_reference_is_a_pull_request_here_then_an_old_issue_then_the_workspace() {
203 let db = legacy();
204 finish(&db);
205 // In web, #4 is its own pull request; #3 its old issue (now #8); #8 is
206 // that item by its new number; #7 is api's old #1; #3 in api is api's
207 // pull request.
208 assert_eq!(reference(&db, Some("rep_web"), "acme", 4), Some(Named::PullHere));
209 assert_eq!(reference(&db, Some("rep_web"), "acme", 3), Some(Named::Item("iss_web3".into())));
210 assert_eq!(reference(&db, Some("rep_web"), "acme", 8), Some(Named::Item("iss_web3".into())));
211 assert_eq!(reference(&db, Some("rep_web"), "acme", 7), Some(Named::Item("iss_api1".into())));
212 assert_eq!(reference(&db, Some("rep_api"), "acme", 3), Some(Named::PullHere));
213 // An old number of another repository is not this one's: web's #2 is
214 // its own pull request, api's #2 (now #9) needs api or its new number.
215 assert_eq!(reference(&db, Some("rep_web"), "acme", 2), Some(Named::PullHere));
216 // In the workspace alone: its item, else a pull request of one of its
217 // repositories.
218 assert_eq!(reference(&db, None, "acme", 9), Some(Named::Item("iss_api2".into())));
219 assert_eq!(reference(&db, None, "acme", 3), Some(Named::PullThere("rep_api".into())));
220 assert_eq!(reference(&db, None, "acme", 42), None);
221 assert_eq!(reference(&db, None, "acme", 2), Some(Named::PullThere("rep_web".into())));
222 // Another workspace's numbers are its own: globex's #3 is its item,
223 // and its pull request #2 is nothing of acme's.
224 assert_eq!(reference(&db, None, "globex", 3), Some(Named::Item("iss_site1".into())));
225 assert_eq!(reference(&db, None, "globex", 2), Some(Named::PullThere("rep_site".into())));
226}
227
228/// A work item of acme, filed from web, meant to land in web and api.
229fn two_targets(db: &Connection) {
230 db.execute_batch(
231 "INSERT INTO repo_workspaces (repo_id, workspace, path, private) VALUES
232 ('rep_web', 'acme', 'acme/web', 0), ('rep_api', 'acme', 'acme/api', 1), ('rep_docs', 'acme', 'acme/docs', 0);
233 INSERT INTO work_items (id, workspace, number, title, body, author_id, author_name, created_at, updated_at)
234 VALUES ('wki_1', 'acme', 12, 'Sign in with passkeys', '', 'usr_1', 'ana', '', ''),
235 ('wki_2', 'acme', 13, 'Typo', '', 'usr_1', 'ana', '', '');
236 INSERT INTO work_item_repos (item_id, repo_id, role, added_at) VALUES
237 ('wki_1', 'rep_web', 'origin', ''), ('wki_1', 'rep_web2', 'target', ''), ('wki_1', 'rep_api', 'target', ''),
238 ('wki_2', 'rep_docs', 'origin', '');",
239 )
240 .unwrap();
241}
242
243/// Lands `item` in `repo` by `pull` or `commit`: whether it closed.
244fn land(db: &Connection, item: &str, repo: &str, pull: Option<u32>, commit: Option<&str>) -> bool {
245 let resolution = serde_json::json!({ "repo_id": repo, "pull": pull, "commit": commit, "pushed_by": "ana" }).to_string();
246 for sql in &LAND_SQL[..3] {
247 run(db, sql, &[&item, &repo, &"2026-10-17", &"usr_1", &resolution, &pull, &commit]);
248 }
249 db.query_row(LAND_SQL[3], params![item, repo, "2026-10-17"], |row| row.get::<_, String>(0)).optional().unwrap().is_some()
250}
251
252#[test]
253fn a_work_item_closes_once_every_target_has_landed() {
254 let db = database();
255 two_targets(&db);
256 // Landing where it was filed is not where it was meant to land.
257 assert!(!land(&db, "wki_1", "rep_web", Some(14), None));
258 assert!(!land(&db, "wki_1", "rep_web2", Some(3), None), "one target of two");
259 // The same merge told again records nothing twice.
260 assert!(!land(&db, "wki_1", "rep_web2", Some(3), None));
261 assert_eq!(strings(&db, "SELECT state FROM work_items WHERE id = 'wki_1'"), ["open"]);
262 assert!(land(&db, "wki_1", "rep_api", None, Some("abc123")), "the last target closes it");
263 assert_eq!(strings(&db, "SELECT state || ' ' || reason FROM work_items WHERE id = 'wki_1'"), ["closed completed"]);
264 let resolutions: Vec<serde_json::Value> =
265 serde_json::from_str(&strings(&db, "SELECT resolutions FROM work_items WHERE id = 'wki_1'")[0]).unwrap();
266 let landed: Vec<&str> = resolutions.iter().map(|r| r["repo_id"].as_str().unwrap()).collect();
267 assert_eq!(landed, ["rep_web", "rep_web2", "rep_api"]);
268 assert_eq!(
269 strings(&db, "SELECT repo_id FROM work_item_repos WHERE item_id = 'wki_1' AND landed_at IS NOT NULL ORDER BY repo_id"),
270 ["rep_api", "rep_web", "rep_web2"]
271 );
272 // Closed already: landing again closes nothing.
273 assert!(!land(&db, "wki_1", "rep_api", Some(20), None));
274}
275
276#[test]
277fn with_no_targets_what_closes_it_closes_it_and_another_repository_is_touched() {
278 let db = database();
279 two_targets(&db);
280 // Fixed from api, which it was not linked to: linked as touched, and,
281 // with no targets, closed.
282 assert!(land(&db, "wki_2", "rep_api", Some(5), None));
283 assert_eq!(
284 strings(&db, "SELECT repo_id || ' ' || role FROM work_item_repos WHERE item_id = 'wki_2' ORDER BY repo_id"),
285 ["rep_api touched", "rep_docs origin"]
286 );
287}
288
289#[test]
290fn part_of_links_without_closing() {
291 let db = database();
292 two_targets(&db);
293 let part = |item: &str, repo: &str| -> bool {
294 db.query_row(PART_SQL, params![item, repo, "2026-10-17", "usr_1"], |row| row.get::<_, String>(0)).optional().unwrap().is_some()
295 };
296 assert!(part("wki_1", "rep_docs"), "linked the first time");
297 assert!(!part("wki_1", "rep_docs"), "and once");
298 assert!(!part("wki_1", "rep_api"), "a target stays a target");
299 assert_eq!(strings(&db, "SELECT role FROM work_item_repos WHERE item_id = 'wki_1' AND repo_id = 'rep_api'"), ["target"]);
300 assert_eq!(strings(&db, "SELECT state FROM work_items WHERE id = 'wki_1'"), ["open"]);
301 assert!(strings(&db, "SELECT landed_at FROM work_item_repos WHERE item_id = 'wki_1' AND landed_at IS NOT NULL").is_empty());
302}
303
304fn next(db: &Connection, workspace: &str, repo: Option<&str>) -> u32 {
305 db.query_row(NEXT_NUMBER, params![workspace, repo], |row| row.get(0)).unwrap()
306}
307
308#[test]
309fn new_numbers_go_on_from_the_workspace_and_never_reuse_a_repositorys() {
310 let db = legacy();
311 finish(&db);
312 // A pull request of web, then a work item: acme's sequence.
313 assert_eq!(next(&db, "acme", Some("rep_web")), 11);
314 assert_eq!(next(&db, "acme", None), 12);
315 // A repository with pull requests up to #50 moves into acme.
316 db.execute_batch(
317 "INSERT INTO pulls (id, repo_id, number, title, agent, runtime, author_id, author_name, created_at, updated_at)
318 VALUES ('pr_big', 'rep_big', 50, 'Old', 'ana', 'external', 'usr_1', 'ana', '', '');",
319 )
320 .unwrap();
321 run(&db, REMEMBER_REPO, &[&"rep_big", &"globex", &"globex/big", &0]);
322 let values = g1t_kit::transfer::values("acme/big", "globex/big", "rep_big");
323 for sql in TRANSFERRED {
324 let binds: Vec<&dyn rusqlite::ToSql> = values.iter().map(|value| value as &dyn rusqlite::ToSql).collect();
325 run(&db, sql, &binds);
326 }
327 assert_eq!(strings(&db, "SELECT workspace || ' ' || path FROM repo_workspaces WHERE repo_id = 'rep_big'"), ["acme acme/big"]);
328 assert_eq!(numbers(&db, "SELECT workspace, last FROM work_counters WHERE workspace = 'acme'"), [("acme".to_owned(), Some(50))]);
329 assert_eq!(next(&db, "acme", Some("rep_big")), 51);
330 // Even had the transfer not been heard of, its own pull requests'
331 // numbers are never taken again.
332 db.execute("UPDATE work_counters SET last = 20 WHERE workspace = 'acme'", []).unwrap();
333 assert_eq!(next(&db, "acme", Some("rep_big")), 51);
334 assert_eq!(next(&db, "acme", Some("rep_web")), 52);
335}
336
337#[test]
338fn a_workspaces_first_number_is_after_its_repositories_before_the_job_runs() {
339 let db = legacy();
340 // Pull requests opened while the job runs: after web's own numbers.
341 assert_eq!(next(&db, "acme", Some("rep_web")), 6);
342 assert_eq!(next(&db, "acme", Some("rep_api")), 7);
343 finish(&db);
344 // The job numbers the moved issues after those.
345 assert_eq!(numbers(&db, "SELECT id, number FROM work_items WHERE id = 'iss_web1'"), [("iss_web1".to_owned(), Some(8))]);
346 // Whichever repository opens one first, none takes a number another
347 // already used for an issue: api's first is 4, and web's after its #5.
348 let db = legacy();
349 assert_eq!(next(&db, "acme", Some("rep_api")), 4);
350 assert_eq!(next(&db, "acme", Some("rep_web")), 6);
351}