diff --git a/crates/workspace-server/src/authority.rs b/crates/workspace-server/src/authority.rs index 6c5e9242..1aa12594 100644 --- a/crates/workspace-server/src/authority.rs +++ b/crates/workspace-server/src/authority.rs @@ -428,8 +428,23 @@ impl TicketAuthority for SqliteWorkspaceAuthority { event_cursor: None, }, )?; - if ticket_matches_query(&summary, &detail, &query) { - items.push(ticket_query_item(summary, &detail, &query)); + let authoritative = self + .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); @@ -1189,6 +1204,8 @@ fn parse_query_cursor(cursor: &str) -> Result<(String, String)> { fn ticket_matches_query( summary: &TicketSummary, detail: &TicketDetail, + authoritative_body: &str, + authoritative_events: &[TicketEvent], query: &TicketQueryRequest, ) -> bool { if !query.states.is_empty() && !query.states.iter().any(|state| state == &summary.state) { @@ -1206,10 +1223,12 @@ fn ticket_matches_query( return false; } if !query.event_kinds.is_empty() - && !detail - .events - .iter() - .any(|event| query.event_kinds.iter().any(|kind| kind == &event.kind)) + && !authoritative_events.iter().any(|event| { + query + .event_kinds + .iter() + .any(|kind| kind == event.kind.as_str()) + }) { return false; } @@ -1275,34 +1294,69 @@ fn ticket_matches_query( }) { return false; } - if let Some(ticket_id) = &query.related_ticket_id { - let relation_json = serde_json::to_string(&detail.relations).unwrap_or_default(); - if !relation_json.contains(ticket_id) { - return false; - } + if let Some(ticket_id) = &query.related_ticket_id + && !detail + .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; } - if let Some(kind) = &query.relation_kind { - let relation_json = serde_json::to_string(&detail.relations).unwrap_or_default(); - if !relation_json.contains(kind) { - return false; - } + if let Some(kind) = &query.relation_kind + && !detail + .relations + .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; } query.query.as_ref().is_none_or(|text| { let needle = text.to_lowercase(); summary.title.to_lowercase().contains(&needle) - || detail.body.to_lowercase().contains(&needle) - || detail.events.iter().any(|event| { - event - .body - .as_deref() - .is_some_and(|body| body.to_lowercase().contains(&needle)) - }) + || authoritative_body.to_lowercase().contains(&needle) + || authoritative_events + .iter() + .any(|event| event.body.as_str().to_lowercase().contains(&needle)) }) } fn ticket_query_item( summary: TicketSummary, detail: &TicketDetail, + authoritative_body: &str, + authoritative_events: &[TicketEvent], query: &TicketQueryRequest, ) -> TicketQueryItem { let mut matched_fields = Vec::new(); @@ -1314,26 +1368,18 @@ fn ticket_query_item( matched_fields.push("title".to_string()); 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()); - 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| { - event - .body - .as_deref() - .is_some_and(|body| body.to_lowercase().contains(&needle)) - }) { + if let Some((sequence, event)) = authoritative_events + .iter() + .enumerate() + .find(|(_, event)| event.body.as_str().to_lowercase().contains(&needle)) + { matched_fields.push("event".to_string()); - matching_event = Some(TicketEvidenceEvent { - event_ref: event.event_ref.clone(), - 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()); + matching_event = Some(ticket_evidence_event(sequence, event)); + snippet.get_or_insert_with(|| event.body.as_str().to_string()); } } TicketQueryItem { @@ -1826,6 +1872,42 @@ mod tests { ); 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(); assert_eq!(tickets.record_authority, "workspace-sqlite"); assert_eq!(tickets.items[0].record_source, "sqlite_yoi_ticket"); @@ -1838,12 +1920,14 @@ mod tests { let ticket = authority.ticket("00000000001J2").unwrap(); assert!(ticket.body.contains("Ticket body")); + assert!(ticket.body_truncated); + assert!(!ticket.body.contains("Deep Ticket marker")); assert!(!ticket.item_revision.is_empty()); assert_eq!(ticket.linked_objectives[0].id, "00000000001J3"); assert_eq!(ticket.event_page.returned, ticket.events.len()); let ticket_query = authority .query_tickets(TicketQueryRequest { - query: Some("Ticket body".to_string()), + query: Some("Deep Ticket marker".to_string()), states: vec!["ready".to_string()], linked_objective_id: Some("00000000001J3".to_string()), attention: vec!["unblocked".to_string(), "missing_commit".to_string()], @@ -1859,6 +1943,44 @@ mod tests { .contains(&"body".to_string()) ); 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 .query_tickets(TicketQueryRequest { sort: Some("title".to_string()), @@ -2052,7 +2174,10 @@ updated_at: "2026-01-02T00:00:00Z" --- Ticket body. +{padding} +Deep Ticket marker. "#, + padding = "x".repeat(70_000), ), ) .unwrap();