test(storage): pin FTS5 contract for search_transcripts

search_transcripts already backs onto the transcripts_fts virtual table
(migration v4, trigger-maintained) via MATCH + ORDER BY rank. Adding a
test to lock the behaviour: token matching is case-insensitive, rank-
ordered, and non-matching tokens return nothing. This is Phase G of the
post-OpenWhispr audit — semantic embeddings stay deferred until the FTS
experience actually hits a wall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 07:55:52 +01:00
parent ba0d59f563
commit 42b32a4f1a

View File

@@ -888,6 +888,61 @@ mod tests {
assert!(deleted.is_none());
}
#[tokio::test]
async fn search_transcripts_uses_fts5_and_ranks_by_relevance() {
// Locks in the FTS5 behaviour contract: MATCH-based substring-like
// search (case-insensitive tokens) with rank-ordered results, so
// anyone swapping `search_transcripts` out for embeddings later
// knows what invariants they must preserve.
let pool = test_pool().await;
let rows = [
("t1", "The Parakeet speech model runs on sherpa-onnx."),
(
"t2",
"Parakeet parakeet parakeet dominates English benchmarks.",
),
("t3", "Whisper large-v3 remains the multilingual champion."),
];
for (id, text) in rows {
insert_transcript(
&pool,
&InsertTranscriptParams {
id,
text,
source: "microphone",
profile_id: crate::DEFAULT_PROFILE_ID,
title: None,
audio_path: None,
duration: 1.0,
engine: Some("whisper"),
model_id: Some("whisper-tiny-en"),
inference_ms: None,
sample_rate: None,
audio_channels: None,
format_mode: None,
remove_fillers: false,
british_english: false,
anti_hallucination: false,
},
)
.await
.unwrap();
}
let hits = search_transcripts(&pool, "parakeet", 10).await.unwrap();
let ids: Vec<&str> = hits.iter().map(|row| row.id.as_str()).collect();
assert_eq!(
ids,
vec!["t2", "t1"],
"t3 has no 'parakeet' token; t2 outranks t1 on repetition"
);
let no_match = search_transcripts(&pool, "moonshine", 10).await.unwrap();
assert!(no_match.is_empty());
}
#[tokio::test]
async fn task_crud_roundtrip() {
let pool = test_pool().await;