fix: align queue eligibility with target authority

This commit is contained in:
2026-08-25 12:24:31 +09:00
parent 5857e6121c
commit 1749757036
2 changed files with 80 additions and 8 deletions
+15 -5
View File
@@ -704,6 +704,15 @@ impl SqliteWorkspaceAuthority {
&self, &self,
reference: &str, reference: &str,
request: TicketShowRequest, request: TicketShowRequest,
) -> Result<TicketDetail> {
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<TicketDetail> { ) -> Result<TicketDetail> {
let id = self let id = self
.store .store
@@ -713,19 +722,19 @@ impl SqliteWorkspaceAuthority {
reference, reference,
)? )?
.ok_or_else(|| Error::Ticket(ticket::TicketError::NotFound(reference.to_string())))?; .ok_or_else(|| Error::Ticket(ticket::TicketError::NotFound(reference.to_string())))?;
let ticket = self.ticket_backend.show(TicketIdOrSlug::Id(id))?; let ticket = backend.show(TicketIdOrSlug::Id(id))?;
self.ticket_detail_from_ticket(ticket, request) self.ticket_detail_from_ticket(ticket, request, backend)
} }
fn ticket_detail_from_ticket( fn ticket_detail_from_ticket(
&self, &self,
ticket: ticket::Ticket, ticket: ticket::Ticket,
request: TicketShowRequest, request: TicketShowRequest,
dependency_backend: &SqliteTicketBackend,
) -> Result<TicketDetail> { ) -> Result<TicketDetail> {
let id = ticket.meta.id.as_str(); let id = ticket.meta.id.as_str();
let dependency_check = self let dependency_check =
.ticket_backend dependency_backend.dependency_check(TicketIdOrSlug::Id(id.to_string()))?;
.dependency_check(TicketIdOrSlug::Id(id.to_string()))?;
let (body, body_truncated) = let (body, body_truncated) =
truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT); truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT);
let event_limit = request let event_limit = request
@@ -1117,6 +1126,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
event_limit: Some(TICKET_EVENT_LIMIT), event_limit: Some(TICKET_EVENT_LIMIT),
event_cursor: None, event_cursor: None,
}, },
&self.ticket_backend,
)?; )?;
if ticket_matches_query( if ticket_matches_query(
&summary, &summary,
+65 -3
View File
@@ -3133,7 +3133,7 @@ async fn scoped_get_ticket(
AxumPath(path): AxumPath<ScopedRecordPath>, AxumPath(path): AxumPath<ScopedRecordPath>,
) -> ApiResult<Json<TicketDetail>> { ) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?; 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( async fn scoped_query_tickets(
@@ -3151,7 +3151,10 @@ async fn scoped_show_ticket(
Json(query): Json<TicketShowRequest>, Json(query): Json<TicketShowRequest>,
) -> ApiResult<Json<TicketDetail>> { ) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?; 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)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -3766,7 +3769,12 @@ fn browser_ticket_backend(api: &WorkspaceApi) -> Result<SqliteTicketBackend> {
} }
fn browser_ticket_detail(api: &WorkspaceApi, ticket_id: &str) -> ApiResult<Json<TicketDetail>> { fn browser_ticket_detail(api: &WorkspaceApi, ticket_id: &str) -> ApiResult<Json<TicketDetail>> {
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( 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] #[tokio::test]
async fn queue_requires_assignments_for_every_ready_dependency() { async fn queue_requires_assignments_for_every_ready_dependency() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();