A run on an issue names its work item, not a pull request
Runs g1t's agent started on an issue (implement, answer; no pull request) kept the issue's number in its repository, which the move to work items never rewrote, and the workspace's sessions showed it as a pull request of that number: another change entirely. Such a run now reports no pull request number, and the sessions list names the work item the issue became, found through work_item_repos.legacy_number when read.
- G1T-Session:
- ses_01m56w5m6cegebbtcjh7ax0a5g
2 files+79−100/2 viewed
| 127 | 127 | } | |
| 128 | 128 | } | |
| 129 | 129 | ||
| 130 | − | /// A run of g1t's agent, as one of the workspace's sessions. | |
| 131 | − | pub(crate) fn from_run(run: AgentRun, repo_id: &str) -> WorkspaceSession { | |
| 130 | + | /// A run of g1t's agent, as one of the workspace's sessions: `item`, the | |
| 131 | + | /// work item one started on an issue worked on. | |
| 132 | + | pub(crate) fn from_run(run: AgentRun, repo_id: &str, item: Option<u32>) -> WorkspaceSession { | |
| 132 | 133 | let state = if run.status.is_active() { SessionState::Active } else { SessionState::Ended }; | |
| 133 | 134 | WorkspaceSession { | |
| 134 | 135 | kind: SessionKind::Agent, | |
| ⋯ | |||
| 140 | 141 | harness: None, | |
| 141 | 142 | agent: Some(run.agent), | |
| 142 | 143 | branch: None, | |
| 143 | − | item: None, | |
| 144 | + | item, | |
| 144 | 145 | item_title: None, | |
| 145 | 146 | pull: run.number, | |
| 146 | 147 | pull_title: None, | |
| ⋯ | |||
| 333 | 334 | self.agent_runs_in(&workspace, &ids, a.shown == SessionsShown::Live, member, limit) | |
| 334 | 335 | .await? | |
| 335 | 336 | .into_iter() | |
| 336 | − | .map(|(repo_id, run)| from_run(run, &repo_id)) | |
| 337 | + | .map(|(repo_id, run, item)| from_run(run, &repo_id, item)) | |
| 337 | 338 | .collect() | |
| 338 | 339 | } else { | |
| 339 | 340 | Vec::new() | |
| ⋯ | |||
| 667 | 668 | }) | |
| 668 | 669 | .collect(); | |
| 669 | 670 | let wanted = Wanted { shown: SessionsShown::Recent, kind: None, harness: None, person: None, item: None }; | |
| 670 | − | let all = merged(Vec::new(), vec![from_run(run(8, "running"), "rep_api")], recorded, &wanted, 10); | |
| 671 | + | let all = merged(Vec::new(), vec![from_run(run(8, "running"), "rep_api", None)], recorded, &wanted, 10); | |
| 671 | 672 | let kinds: Vec<(SessionKind, &str)> = all.iter().map(|session| (session.kind, session.id.as_str())).collect(); | |
| 672 | 673 | assert_eq!(kinds, [(SessionKind::Agent, "run_1"), (SessionKind::Recorded, "pr_api_7")]); | |
| 673 | 674 | assert_eq!(all[0].actor.as_deref(), Some("g1t")); | |
| ⋯ | |||
| 679 | 680 | assert!(merged(Vec::new(), Vec::new(), vec![(all[1].clone(), false)], &by_harness, 10).is_empty()); | |
| 680 | 681 | // A finished run is not live. | |
| 681 | 682 | let live = Wanted { shown: SessionsShown::Live, kind: None, harness: None, person: None, item: None }; | |
| 682 | − | assert!(merged(Vec::new(), vec![from_run(run(8, "succeeded"), "rep_api")], Vec::new(), &live, 10).is_empty()); | |
| 683 | + | assert!(merged(Vec::new(), vec![from_run(run(8, "succeeded"), "rep_api", None)], Vec::new(), &live, 10).is_empty()); | |
| 683 | 684 | } | |
| 684 | 685 | ||
| 685 | 686 | #[test] | |
| 66 | 66 | const SESSION_ENTRIES: u32 = 2000; | |
| 67 | 67 | ||
| 68 | 68 | /// Every column but the steps, which only a run's own page needs. | |
| 69 | + | /// | |
| 70 | + | /// A run g1t's agent started on an issue (no pull request; only these | |
| 71 | + | /// kinds) kept the issue's number in its repository, which the move to | |
| 72 | + | /// work items (g1t #57) never rewrote: `on_item` says the number is not a | |
| 73 | + | /// pull request's, and `item` is the work item's number in its workspace, | |
| 74 | + | /// found through the issue's old number. | |
| 69 | 75 | const COLUMNS: &str = "id, workspace, repo_id, repo, number, pull_id, kind, agent, model, status, | |
| 70 | 76 | step, step_count, cost_usd, turns, sandbox, token_hash, started_by, error, created_at, | |
| 71 | 77 | started_at, finished_at, updated_at, budget_usd, time_cap_minutes, halted, | |
| 72 | − | COALESCE((SELECT title FROM pulls WHERE pulls.id = agent_runs.pull_id), agent_runs.title) AS title"; | |
| 78 | + | COALESCE((SELECT title FROM pulls WHERE pulls.id = agent_runs.pull_id), agent_runs.title) AS title, | |
| 79 | + | (agent_runs.pull_id IS NULL AND agent_runs.kind IN ('implement', 'answer')) AS on_item, | |
| 80 | + | (SELECT i.number FROM work_item_repos r JOIN work_items i ON i.id = r.item_id | |
| 81 | + | WHERE agent_runs.pull_id IS NULL AND agent_runs.kind IN ('implement', 'answer') | |
| 82 | + | AND r.repo_id = agent_runs.repo_id AND r.legacy_number = agent_runs.number) AS item"; | |
| 73 | 83 | ||
| 74 | 84 | /// The pull requests of the repositories in `?1` (a JSON array of ids) | |
| 75 | 85 | /// with entries recorded on them directly (not by a session moved onto | |
| ⋯ | |||
| 133 | 143 | time_cap_minutes: Option<u32>, | |
| 134 | 144 | #[serde(default)] | |
| 135 | 145 | halted: Option<String>, | |
| 146 | + | /// 1 when `number` is an issue's, not a pull request's (see [`COLUMNS`]). | |
| 147 | + | #[serde(default)] | |
| 148 | + | on_item: Option<u32>, | |
| 149 | + | /// The work item that issue became, by number in its workspace. | |
| 150 | + | #[serde(default)] | |
| 151 | + | item: Option<u32>, | |
| 136 | 152 | } | |
| 137 | 153 | ||
| 138 | 154 | impl RunRow { | |
| 155 | + | fn on_item(&self) -> bool { | |
| 156 | + | self.on_item.is_some_and(|flag| flag != 0) | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | /// The work item it worked on, for a run started on an issue. | |
| 160 | + | pub(crate) fn item(&self) -> Option<u32> { | |
| 161 | + | self.item.filter(|_| self.on_item()) | |
| 162 | + | } | |
| 163 | + | ||
| 139 | 164 | fn status(&self) -> RunStatus { | |
| 140 | 165 | RunStatus::parse(&self.status).unwrap_or(RunStatus::Failed) | |
| 141 | 166 | } | |
| ⋯ | |||
| 143 | 168 | /// The run as `member` (or not) may see it: model and cost are the | |
| 144 | 169 | /// workspace's business. | |
| 145 | 170 | fn into_run(self, member: bool) -> AgentRun { | |
| 171 | + | let on_item = self.on_item(); | |
| 146 | 172 | let (namespace, name) = self.repo.split_once('/').unwrap_or((&self.repo, "")); | |
| 147 | 173 | AgentRun { | |
| 148 | 174 | repo: RepoPath { | |
| ⋯ | |||
| 157 | 183 | .and_then(|steps| serde_json::from_str(steps).ok()) | |
| 158 | 184 | .unwrap_or_default(), | |
| 159 | 185 | id: self.id, | |
| 160 | − | number: self.number.filter(|number| *number > 0), | |
| 186 | + | // An issue's old number is no pull request's. | |
| 187 | + | number: self.number.filter(|number| *number > 0 && !on_item), | |
| 161 | 188 | title: self.title, | |
| 162 | 189 | agent: self.agent, | |
| 163 | 190 | model: self.model.filter(|_| member), | |
| ⋯ | |||
| 484 | 511 | /// (sandboxes for checks, the queue and merge checks are not agents), | |
| 485 | 512 | /// newest first, with each one's repository id. Only `live` ones: those | |
| 486 | 513 | /// queued or running. | |
| 487 | − | pub(crate) async fn agent_runs_in(&self, workspace: &str, repo_ids: &[String], live: bool, member: bool, limit: u32) -> Result<Vec<(String, AgentRun)>> { | |
| 514 | + | /// Each with its repository's id, and the work item it worked on, by | |
| 515 | + | /// number, for one started on an issue. | |
| 516 | + | pub(crate) async fn agent_runs_in( | |
| 517 | + | &self, | |
| 518 | + | workspace: &str, | |
| 519 | + | repo_ids: &[String], | |
| 520 | + | live: bool, | |
| 521 | + | member: bool, | |
| 522 | + | limit: u32, | |
| 523 | + | ) -> Result<Vec<(String, AgentRun, Option<u32>)>> { | |
| 488 | 524 | if repo_ids.is_empty() { | |
| 489 | 525 | return Ok(Vec::new()); | |
| 490 | 526 | } | |
| ⋯ | |||
| 501 | 537 | .all() | |
| 502 | 538 | .await? | |
| 503 | 539 | .results::<RunRow>()?; | |
| 504 | − | Ok(rows.into_iter().map(|row| (row.repo_id.clone(), row.into_run(member))).collect()) | |
| 540 | + | Ok(rows | |
| 541 | + | .into_iter() | |
| 542 | + | .map(|row| { | |
| 543 | + | let item = row.item(); | |
| 544 | + | (row.repo_id.clone(), row.into_run(member), item) | |
| 545 | + | }) | |
| 546 | + | .collect()) | |
| 505 | 547 | } | |
| 506 | 548 | ||
| 507 | 549 | /// The pull requests of some repositories with a recorded session, | |
| ⋯ | |||
| 707 | 749 | } | |
| 708 | 750 | ||
| 709 | 751 | #[test] | |
| 752 | + | fn a_run_on_an_issue_names_its_work_item_not_a_pull_request() { | |
| 753 | + | let db = crate::migrated::database(); | |
| 754 | + | db.execute_batch( | |
| 755 | + | "INSERT INTO work_items (id, workspace, number, title, body, author_id, author_name, created_at, updated_at) | |
| 756 | + | VALUES ('iss_7', 'acme', 57, 'Passkeys', '', 'usr_1', 'ana', '', ''); | |
| 757 | + | INSERT INTO work_item_repos (item_id, repo_id, role, legacy_number, added_at) VALUES ('iss_7', 'rep_web', 'origin', 7, ''); | |
| 758 | + | INSERT INTO agent_runs (id, workspace, repo_id, repo, number, pull_id, kind, agent, status, sandbox, token_hash, created_at, updated_at) | |
| 759 | + | VALUES ('arn_issue', 'acme', 'rep_web', 'acme/web', 7, NULL, 'implement', 'g1t', 'succeeded', 's', 't', '', ''), | |
| 760 | + | ('arn_pull', 'acme', 'rep_web', 'acme/web', 7, 'pul_7', 'review', 'g1t', 'succeeded', 's', 't', '', ''), | |
| 761 | + | ('arn_queue', 'acme', 'rep_web', 'acme/web', 7, NULL, 'queue', 'g1t', 'succeeded', 's', 't', '', '');", | |
| 762 | + | ) | |
| 763 | + | .unwrap(); | |
| 764 | + | let run = |id: &str| { | |
| 765 | + | db.query_row(&format!("SELECT on_item, item FROM (SELECT {COLUMNS} FROM agent_runs WHERE id = ?1)"), [id], |row| { | |
| 766 | + | Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?)) | |
| 767 | + | }) | |
| 768 | + | .unwrap() | |
| 769 | + | }; | |
| 770 | + | // Issue #7 of web is work item #57 of acme; pull request #7 is not it. | |
| 771 | + | assert_eq!(run("arn_issue"), (1, Some(57))); | |
| 772 | + | assert_eq!(run("arn_pull"), (0, None)); | |
| 773 | + | // The merge queue names a pull request by number alone. | |
| 774 | + | assert_eq!(run("arn_queue"), (0, None)); | |
| 775 | + | } | |
| 776 | + | ||
| 777 | + | #[test] | |
| 710 | 778 | fn steps_are_one_short_line() { | |
| 711 | 779 | assert_eq!(one_line(" Read\n src/lib.rs ", 40), "Read src/lib.rs"); | |
| 712 | 780 | let long = one_line(&"x".repeat(300), 10); | |
Comments
Sign in to comment.