From 17497570361557a5c14b924beaf020f59c4a4f43 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 25 Aug 2026 12:24:31 +0900 Subject: [PATCH] fix: align queue eligibility with target authority --- crates/workspace-server/src/authority.rs | 20 +++++-- crates/workspace-server/src/server.rs | 68 ++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/crates/workspace-server/src/authority.rs b/crates/workspace-server/src/authority.rs index 16f9e0f8..1d01aad7 100644 --- a/crates/workspace-server/src/authority.rs +++ b/crates/workspace-server/src/authority.rs @@ -704,6 +704,15 @@ impl SqliteWorkspaceAuthority { &self, reference: &str, request: TicketShowRequest, + ) -> Result { + self.read_ticket_detail_with_backend(reference, request, &self.ticket_backend) + } + + pub(crate) fn read_ticket_detail_with_backend( + &self, + reference: &str, + request: TicketShowRequest, + backend: &SqliteTicketBackend, ) -> Result { let id = self .store @@ -713,19 +722,19 @@ impl SqliteWorkspaceAuthority { reference, )? .ok_or_else(|| Error::Ticket(ticket::TicketError::NotFound(reference.to_string())))?; - let ticket = self.ticket_backend.show(TicketIdOrSlug::Id(id))?; - self.ticket_detail_from_ticket(ticket, request) + let ticket = backend.show(TicketIdOrSlug::Id(id))?; + self.ticket_detail_from_ticket(ticket, request, backend) } fn ticket_detail_from_ticket( &self, ticket: ticket::Ticket, request: TicketShowRequest, + dependency_backend: &SqliteTicketBackend, ) -> Result { let id = ticket.meta.id.as_str(); - let dependency_check = self - .ticket_backend - .dependency_check(TicketIdOrSlug::Id(id.to_string()))?; + let dependency_check = + dependency_backend.dependency_check(TicketIdOrSlug::Id(id.to_string()))?; let (body, body_truncated) = truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT); let event_limit = request @@ -1117,6 +1126,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority { event_limit: Some(TICKET_EVENT_LIMIT), event_cursor: None, }, + &self.ticket_backend, )?; if ticket_matches_query( &summary, diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 3ab662e9..367e3055 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -3133,7 +3133,7 @@ async fn scoped_get_ticket( AxumPath(path): AxumPath, ) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; - get_ticket(State(api), AxumPath(path.id)).await + browser_ticket_detail(&api, &path.id) } async fn scoped_query_tickets( @@ -3151,7 +3151,10 @@ async fn scoped_show_ticket( Json(query): Json, ) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; - Ok(Json(api.authority.show_ticket(&path.id, query)?)) + let backend = browser_ticket_backend(&api)?; + Ok(Json(api.authority.read_ticket_detail_with_backend( + &path.id, query, &backend, + )?)) } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -3766,7 +3769,12 @@ fn browser_ticket_backend(api: &WorkspaceApi) -> Result { } fn browser_ticket_detail(api: &WorkspaceApi, ticket_id: &str) -> ApiResult> { - Ok(Json(api.authority.ticket(ticket_id)?)) + let backend = browser_ticket_backend(api)?; + Ok(Json(api.authority.read_ticket_detail_with_backend( + ticket_id, + TicketShowRequest::default(), + &backend, + )?)) } async fn scoped_edit_ticket_item( @@ -16521,6 +16529,60 @@ mod tests { } } + #[tokio::test] + async fn browser_queue_eligibility_uses_authoritative_dependency_targets() { + let dir = tempfile::tempdir().unwrap(); + init_clean_git_workspace(dir.path()); + let api = test_api(dir.path()).await; + let backend = browser_ticket_backend(&api).unwrap(); + let mut dependency_input = ticket::NewTicket::new("Invalid target dependency"); + dependency_input.workflow_state = Some(TicketWorkflowState::Ready); + dependency_input.repository_id = Some(TEST_REPOSITORY_ID.to_string()); + dependency_input.ref_selector = Some("missing-ref".to_string()); + let dependency = backend.create(dependency_input).unwrap(); + let mut root_input = ticket::NewTicket::new("Queue root"); + root_input.workflow_state = Some(TicketWorkflowState::Ready); + root_input.repository_id = Some(TEST_REPOSITORY_ID.to_string()); + root_input.ref_selector = Some("develop".to_string()); + let root = backend.create(root_input).unwrap(); + backend + .add_ticket_relation( + root.id.clone().into(), + ticket::NewTicketRelation { + kind: ticket::TicketRelationKind::DependsOn, + target: dependency.id.clone(), + note: None, + author: Some("test".to_string()), + }, + ) + .unwrap(); + assign_test_orchestrator(&api, &root.id); + assign_test_orchestrator(&api, &dependency.id); + + let Json(detail) = browser_ticket_detail(&api, &root.id).unwrap(); + assert!(!detail.action_eligibility.can_queue); + assert!( + detail + .action_eligibility + .blockers + .iter() + .any(|blocker| blocker.contains("missing-ref")) + ); + let result = scoped_queue_ticket_record( + State(api.clone()), + AxumPath((TEST_WORKSPACE_ID.to_string(), root.id.clone())), + HeaderMap::new(), + ) + .await; + assert!(result.is_err()); + for ticket_id in [dependency.id, root.id] { + assert_eq!( + backend.show(ticket_id.into()).unwrap().meta.workflow_state, + TicketWorkflowState::Ready + ); + } + } + #[tokio::test] async fn queue_requires_assignments_for_every_ready_dependency() { let dir = tempfile::tempdir().unwrap();