fix: query authoritative Ticket content and relations

This commit is contained in:
2026-08-17 06:46:43 +09:00
parent 45b7d0764d
commit 7507412f1c
+163 -38
View File
@@ -428,8 +428,23 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
event_cursor: None, event_cursor: None,
}, },
)?; )?;
if ticket_matches_query(&summary, &detail, &query) { let authoritative = self
items.push(ticket_query_item(summary, &detail, &query)); .ticket_backend
.show(TicketIdOrSlug::Id(summary.id.clone()))?;
if ticket_matches_query(
&summary,
&detail,
authoritative.document.body.as_str(),
&authoritative.events,
&query,
) {
items.push(ticket_query_item(
summary,
&detail,
authoritative.document.body.as_str(),
&authoritative.events,
&query,
));
} }
} }
sort_ticket_query_items(&mut items, sort); sort_ticket_query_items(&mut items, sort);
@@ -1189,6 +1204,8 @@ fn parse_query_cursor(cursor: &str) -> Result<(String, String)> {
fn ticket_matches_query( fn ticket_matches_query(
summary: &TicketSummary, summary: &TicketSummary,
detail: &TicketDetail, detail: &TicketDetail,
authoritative_body: &str,
authoritative_events: &[TicketEvent],
query: &TicketQueryRequest, query: &TicketQueryRequest,
) -> bool { ) -> bool {
if !query.states.is_empty() && !query.states.iter().any(|state| state == &summary.state) { if !query.states.is_empty() && !query.states.iter().any(|state| state == &summary.state) {
@@ -1206,10 +1223,12 @@ fn ticket_matches_query(
return false; return false;
} }
if !query.event_kinds.is_empty() if !query.event_kinds.is_empty()
&& !detail && !authoritative_events.iter().any(|event| {
.events query
.event_kinds
.iter() .iter()
.any(|event| query.event_kinds.iter().any(|kind| kind == &event.kind)) .any(|kind| kind == event.kind.as_str())
})
{ {
return false; return false;
} }
@@ -1275,34 +1294,69 @@ fn ticket_matches_query(
}) { }) {
return false; return false;
} }
if let Some(ticket_id) = &query.related_ticket_id { if let Some(ticket_id) = &query.related_ticket_id
let relation_json = serde_json::to_string(&detail.relations).unwrap_or_default(); && !detail
if !relation_json.contains(ticket_id) { .relations
.outgoing
.iter()
.any(|relation| &relation.target == ticket_id)
&& !detail
.relations
.incoming
.iter()
.any(|relation| &relation.source_ticket == ticket_id)
&& !detail
.relations
.blockers
.iter()
.any(|relation| &relation.blocking_ticket == ticket_id)
&& !detail
.relations
.notices
.iter()
.any(|relation| &relation.related_ticket == ticket_id)
{
return false; return false;
} }
} if let Some(kind) = &query.relation_kind
if let Some(kind) = &query.relation_kind { && !detail
let relation_json = serde_json::to_string(&detail.relations).unwrap_or_default(); .relations
if !relation_json.contains(kind) { .outgoing
.iter()
.any(|relation| &relation.kind == kind)
&& !detail
.relations
.incoming
.iter()
.any(|relation| &relation.forward_kind == kind)
&& !detail
.relations
.blockers
.iter()
.any(|relation| &relation.relation_kind == kind)
&& !detail
.relations
.notices
.iter()
.any(|relation| &relation.kind == kind)
{
return false; return false;
} }
}
query.query.as_ref().is_none_or(|text| { query.query.as_ref().is_none_or(|text| {
let needle = text.to_lowercase(); let needle = text.to_lowercase();
summary.title.to_lowercase().contains(&needle) summary.title.to_lowercase().contains(&needle)
|| detail.body.to_lowercase().contains(&needle) || authoritative_body.to_lowercase().contains(&needle)
|| detail.events.iter().any(|event| { || authoritative_events
event .iter()
.body .any(|event| event.body.as_str().to_lowercase().contains(&needle))
.as_deref()
.is_some_and(|body| body.to_lowercase().contains(&needle))
})
}) })
} }
fn ticket_query_item( fn ticket_query_item(
summary: TicketSummary, summary: TicketSummary,
detail: &TicketDetail, detail: &TicketDetail,
authoritative_body: &str,
authoritative_events: &[TicketEvent],
query: &TicketQueryRequest, query: &TicketQueryRequest,
) -> TicketQueryItem { ) -> TicketQueryItem {
let mut matched_fields = Vec::new(); let mut matched_fields = Vec::new();
@@ -1314,26 +1368,18 @@ fn ticket_query_item(
matched_fields.push("title".to_string()); matched_fields.push("title".to_string());
snippet = Some(summary.title.clone()); snippet = Some(summary.title.clone());
} }
if detail.body.to_lowercase().contains(&needle) { if authoritative_body.to_lowercase().contains(&needle) {
matched_fields.push("body".to_string()); matched_fields.push("body".to_string());
snippet.get_or_insert_with(|| matching_snippet(&detail.body, text)); snippet.get_or_insert_with(|| matching_snippet(authoritative_body, text));
} }
if let Some(event) = detail.events.iter().find(|event| { if let Some((sequence, event)) = authoritative_events
event .iter()
.body .enumerate()
.as_deref() .find(|(_, event)| event.body.as_str().to_lowercase().contains(&needle))
.is_some_and(|body| body.to_lowercase().contains(&needle)) {
}) {
matched_fields.push("event".to_string()); matched_fields.push("event".to_string());
matching_event = Some(TicketEvidenceEvent { matching_event = Some(ticket_evidence_event(sequence, event));
event_ref: event.event_ref.clone(), snippet.get_or_insert_with(|| event.body.as_str().to_string());
sequence: event.sequence,
kind: event.kind.clone(),
at: event.at.clone(),
author: event.author.clone(),
excerpt: event.body.clone().unwrap_or_default(),
});
snippet.get_or_insert_with(|| event.body.clone().unwrap_or_default());
} }
} }
TicketQueryItem { TicketQueryItem {
@@ -1826,6 +1872,42 @@ mod tests {
); );
let authority = SqliteWorkspaceAuthority::new(&db_path, "workspace-test").unwrap(); let authority = SqliteWorkspaceAuthority::new(&db_path, "workspace-test").unwrap();
authority
.ticket_backend
.add_event(
TicketIdOrSlug::Id("00000000001J2".to_string()),
ticket::NewTicketEvent::new(
ticket::TicketEventKind::Other("historical_signal".to_string()),
"Historical event marker.",
),
)
.unwrap();
for index in 0..110 {
authority
.ticket_backend
.add_event(
TicketIdOrSlug::Id("00000000001J2".to_string()),
ticket::NewTicketEvent::new(
ticket::TicketEventKind::Comment,
format!("Filler event {index}."),
),
)
.unwrap();
}
authority
.ticket_backend
.add_ticket_relation(
TicketIdOrSlug::Id("00000000001J2".to_string()),
ticket::NewTicketRelation {
kind: ticket::TicketRelationKind::Related,
target: "00000000001J5".to_string(),
note: Some(
"mentions unrelated id 00000000001J9 and kind depends_on".to_string(),
),
author: Some("tester".to_string()),
},
)
.unwrap();
let tickets = authority.list_tickets(20).unwrap(); let tickets = authority.list_tickets(20).unwrap();
assert_eq!(tickets.record_authority, "workspace-sqlite"); assert_eq!(tickets.record_authority, "workspace-sqlite");
assert_eq!(tickets.items[0].record_source, "sqlite_yoi_ticket"); assert_eq!(tickets.items[0].record_source, "sqlite_yoi_ticket");
@@ -1838,12 +1920,14 @@ mod tests {
let ticket = authority.ticket("00000000001J2").unwrap(); let ticket = authority.ticket("00000000001J2").unwrap();
assert!(ticket.body.contains("Ticket body")); assert!(ticket.body.contains("Ticket body"));
assert!(ticket.body_truncated);
assert!(!ticket.body.contains("Deep Ticket marker"));
assert!(!ticket.item_revision.is_empty()); assert!(!ticket.item_revision.is_empty());
assert_eq!(ticket.linked_objectives[0].id, "00000000001J3"); assert_eq!(ticket.linked_objectives[0].id, "00000000001J3");
assert_eq!(ticket.event_page.returned, ticket.events.len()); assert_eq!(ticket.event_page.returned, ticket.events.len());
let ticket_query = authority let ticket_query = authority
.query_tickets(TicketQueryRequest { .query_tickets(TicketQueryRequest {
query: Some("Ticket body".to_string()), query: Some("Deep Ticket marker".to_string()),
states: vec!["ready".to_string()], states: vec!["ready".to_string()],
linked_objective_id: Some("00000000001J3".to_string()), linked_objective_id: Some("00000000001J3".to_string()),
attention: vec!["unblocked".to_string(), "missing_commit".to_string()], attention: vec!["unblocked".to_string(), "missing_commit".to_string()],
@@ -1859,6 +1943,44 @@ mod tests {
.contains(&"body".to_string()) .contains(&"body".to_string())
); );
assert_eq!(ticket_query.page.limit, 1); assert_eq!(ticket_query.page.limit, 1);
let historical_event_query = authority
.query_tickets(TicketQueryRequest {
query: Some("Historical event marker".to_string()),
event_kinds: vec!["historical_signal".to_string()],
limit: Some(1),
..TicketQueryRequest::default()
})
.unwrap();
assert_eq!(historical_event_query.items.len(), 1);
let matching_event = historical_event_query.items[0]
.matching_event
.as_ref()
.expect("matching historical event");
assert_eq!(matching_event.kind, "historical_signal");
assert!(matching_event.excerpt.contains("Historical event marker"));
let note_only_id = authority
.query_tickets(TicketQueryRequest {
related_ticket_id: Some("00000000001J9".to_string()),
..TicketQueryRequest::default()
})
.unwrap();
assert!(note_only_id.items.is_empty());
let note_only_kind = authority
.query_tickets(TicketQueryRequest {
relation_kind: Some("depends_on".to_string()),
..TicketQueryRequest::default()
})
.unwrap();
assert!(note_only_kind.items.is_empty());
let exact_relation = authority
.query_tickets(TicketQueryRequest {
related_ticket_id: Some("00000000001J5".to_string()),
relation_kind: Some("related".to_string()),
..TicketQueryRequest::default()
})
.unwrap();
assert_eq!(exact_relation.items.len(), 1);
assert_eq!(exact_relation.items[0].id, "00000000001J2");
let first_page = authority let first_page = authority
.query_tickets(TicketQueryRequest { .query_tickets(TicketQueryRequest {
sort: Some("title".to_string()), sort: Some("title".to_string()),
@@ -2052,7 +2174,10 @@ updated_at: "2026-01-02T00:00:00Z"
--- ---
Ticket body. Ticket body.
{padding}
Deep Ticket marker.
"#, "#,
padding = "x".repeat(70_000),
), ),
) )
.unwrap(); .unwrap();