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.

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