Skip to main content

kcode_speaker_store/
lib.rs

1use kcode_speaker_types::{FeatureVector, Key, LabeledSample, ObjectId, RecordingKind, SegmentRef};
2use rusqlite::{Connection, params};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    error::Error,
8    fmt,
9    path::Path,
10    sync::Mutex,
11};
12
13#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
14pub struct EventEnvelope {
15    pub event_id: Key,
16    pub event: Event,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
20pub enum Event {
21    RegisterSegment(SegmentRegistration),
22    RecordAttempt(AttemptRecord),
23    SelectAttempt(AttemptSelection),
24    SetSampleState(SampleStateChange),
25    PutArtifact(ArtifactRecord),
26    SetActiveModel(ActiveModelChange),
27}
28
29#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
30pub struct SegmentRegistration {
31    pub segment: SegmentRef,
32    pub group_id: Key,
33    pub recording_kind: RecordingKind,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
37pub struct AttemptRecord {
38    pub attempt_id: Key,
39    pub clip_object: ObjectId,
40    pub extraction_key: Key,
41    pub provider_result_object: Option<ObjectId>,
42    pub outcome: StoredAttemptOutcome,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46pub enum StoredAttemptOutcome {
47    Scored {
48        recording_quality: u8,
49        speakers: Vec<StoredSpeaker>,
50    },
51    Unscorable {
52        reason: String,
53    },
54    InvalidResponse {
55        error: String,
56    },
57    ProviderFailure {
58        code: Key,
59        detail: String,
60    },
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
64pub struct StoredSpeaker {
65    pub speaker_ordinal: u16,
66    pub primary_language: Key,
67    pub closest_dialect: String,
68    pub usable_speech_ms: u32,
69    pub features: FeatureVector,
70}
71
72#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73pub struct AttemptSelection {
74    pub clip_object: ObjectId,
75    pub extraction_key: Key,
76    pub attempt_id: Key,
77    pub reason: String,
78}
79
80#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
81pub struct SampleStateChange {
82    pub sample_id: Key,
83    pub state: SampleState,
84    pub reason: String,
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
88pub enum SampleState {
89    Unlabeled,
90    Confirmed { speaker_id: Key },
91    Retracted,
92}
93
94#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
95pub struct ArtifactRecord {
96    pub artifact_id: Key,
97    pub cohort_id: Key,
98    pub kind: ArtifactKind,
99    pub sha256: [u8; 32],
100    pub bytes: Vec<u8>,
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
104pub enum ArtifactKind {
105    Model,
106    Evaluation,
107    ReplayManifest,
108}
109
110#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
111pub struct ActiveModelChange {
112    pub cohort_id: Key,
113    pub model_artifact_id: Option<Key>,
114    pub reason: String,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
118pub enum Request {
119    Commit(Vec<EventEnvelope>),
120    Read(Query),
121}
122
123#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
124pub enum Query {
125    Attempt {
126        attempt_id: Key,
127    },
128    Attempts {
129        extraction_key: Option<Key>,
130        source_object: Option<ObjectId>,
131    },
132    TrainingRows {
133        cohort_id: Key,
134    },
135    RepeatabilityRows {
136        cohort_id: Key,
137    },
138    Artifact {
139        artifact_id: Key,
140    },
141    ActiveModel {
142        cohort_id: Key,
143    },
144    Inventory {
145        extraction_key: Option<Key>,
146    },
147}
148
149#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
150pub struct Response {
151    pub revision: u64,
152    pub result: ResponseKind,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
156pub enum ResponseKind {
157    Commit { applied: u64 },
158    Attempt(Option<AttemptSnapshot>),
159    Attempts(Vec<AttemptSnapshot>),
160    TrainingRows(Vec<LabeledSample>),
161    RepeatabilityRows(Vec<LabeledSample>),
162    Artifact(Option<ArtifactRecord>),
163    ActiveModel(Option<Key>),
164    Inventory(InventorySnapshot),
165}
166
167#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168pub struct AttemptSnapshot {
169    pub attempt: AttemptRecord,
170    pub sample_ids: Vec<Key>,
171    pub selected: bool,
172}
173
174#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
175pub struct SelectionSnapshot {
176    pub selection: AttemptSelection,
177}
178
179#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
180pub struct SampleStateSnapshot {
181    pub change: SampleStateChange,
182}
183
184#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185pub struct ArtifactMetadata {
186    pub artifact_id: Key,
187    pub cohort_id: Key,
188    pub kind: ArtifactKind,
189    pub sha256: [u8; 32],
190    pub byte_len: u64,
191}
192
193#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
194pub struct ActiveModelSnapshot {
195    pub change: ActiveModelChange,
196}
197
198#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
199pub struct MissingSegmentOrdinals {
200    pub source_object: ObjectId,
201    pub ordinals: Vec<u16>,
202}
203
204#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
205pub struct OutcomeCounts {
206    pub scored: u64,
207    pub unscorable: u64,
208    pub invalid_response: u64,
209    pub provider_failure: u64,
210}
211
212#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
213pub struct InventorySnapshot {
214    pub registrations: Vec<SegmentRegistration>,
215    pub attempts: Vec<AttemptSnapshot>,
216    pub selections: Vec<SelectionSnapshot>,
217    pub sample_states: Vec<SampleStateSnapshot>,
218    pub artifacts: Vec<ArtifactMetadata>,
219    pub active_models: Vec<ActiveModelSnapshot>,
220    pub missing_segment_ordinals: Vec<MissingSegmentOrdinals>,
221    pub outcome_counts: OutcomeCounts,
222}
223
224#[derive(Debug)]
225pub enum StoreError {
226    Validation(String),
227    Conflict(String),
228    SchemaVersion(i64),
229    Storage(String),
230}
231
232impl fmt::Display for StoreError {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        match self {
235            Self::Validation(v) => write!(f, "validation: {v}"),
236            Self::Conflict(v) => write!(f, "conflict: {v}"),
237            Self::SchemaVersion(v) => write!(f, "unsupported schema version {v}"),
238            Self::Storage(v) => write!(f, "storage: {v}"),
239        }
240    }
241}
242
243impl Error for StoreError {}
244
245impl From<rusqlite::Error> for StoreError {
246    fn from(value: rusqlite::Error) -> Self {
247        Self::Storage(value.to_string())
248    }
249}
250
251impl From<serde_json::Error> for StoreError {
252    fn from(value: serde_json::Error) -> Self {
253        Self::Storage(value.to_string())
254    }
255}
256
257pub struct Store {
258    connection: Mutex<Connection>,
259}
260
261pub fn open(path: impl AsRef<Path>) -> Result<Store, StoreError> {
262    let connection = Connection::open(path)?;
263    let version: i64 = connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
264    if version == 0 {
265        let objects: i64 = connection.query_row(
266            "SELECT count(*) FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'",
267            [],
268            |row| row.get(0),
269        )?;
270        if objects != 0 {
271            return Err(StoreError::SchemaVersion(0));
272        }
273        connection.execute_batch(
274            "CREATE TABLE events(
275                 event_id TEXT PRIMARY KEY NOT NULL,
276                 event_json BLOB NOT NULL
277             );
278             PRAGMA user_version = 1;",
279        )?;
280    } else if version != 1 {
281        return Err(StoreError::SchemaVersion(version));
282    }
283    connection.prepare("SELECT event_id,event_json FROM events LIMIT 0")?;
284    Ok(Store {
285        connection: Mutex::new(connection),
286    })
287}
288
289impl Store {
290    pub fn execute(&self, request: Request) -> Result<Response, StoreError> {
291        let mut connection = self
292            .connection
293            .lock()
294            .map_err(|_| StoreError::Storage("connection lock poisoned".into()))?;
295        let (mut event_ids, mut projection) = load(&connection)?;
296        match request {
297            Request::Read(query) => Ok(Response {
298                revision: event_ids.len() as u64,
299                result: projection.read(query)?,
300            }),
301            Request::Commit(events) => {
302                let mut additions = Vec::new();
303                for envelope in events {
304                    let id = envelope.event_id.as_ref().to_owned();
305                    let json = serde_json::to_vec(&envelope.event)?;
306                    if let Some(old) = event_ids.get(&id) {
307                        if old != &json {
308                            return Err(StoreError::Conflict(format!(
309                                "event ID {id} has different content"
310                            )));
311                        }
312                        continue;
313                    }
314                    projection.apply(&envelope.event)?;
315                    event_ids.insert(id.clone(), json.clone());
316                    additions.push((id, json));
317                }
318                let transaction = connection.transaction()?;
319                for (id, json) in &additions {
320                    transaction.execute(
321                        "INSERT INTO events(event_id,event_json) VALUES(?1,?2)",
322                        params![id, json],
323                    )?;
324                }
325                transaction.commit()?;
326                Ok(Response {
327                    revision: event_ids.len() as u64,
328                    result: ResponseKind::Commit {
329                        applied: additions.len() as u64,
330                    },
331                })
332            }
333        }
334    }
335}
336
337#[derive(Default)]
338struct Projection {
339    registrations: BTreeMap<ObjectId, SegmentRegistration>,
340    attempts: BTreeMap<Key, AttemptRecord>,
341    selections: BTreeMap<(ObjectId, Key), AttemptSelection>,
342    states: BTreeMap<Key, SampleStateChange>,
343    artifacts: BTreeMap<Key, ArtifactRecord>,
344    active_models: BTreeMap<Key, ActiveModelChange>,
345}
346
347fn load(connection: &Connection) -> Result<(BTreeMap<String, Vec<u8>>, Projection), StoreError> {
348    let mut statement =
349        connection.prepare("SELECT event_id,event_json FROM events ORDER BY rowid")?;
350    let rows = statement.query_map([], |row| {
351        Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
352    })?;
353    let mut ids = BTreeMap::new();
354    let mut projection = Projection::default();
355    for row in rows {
356        let (id, json) = row?;
357        Key::parse(&id)
358            .map_err(|e| StoreError::Storage(format!("invalid stored event ID: {e}")))?;
359        let event: Event = serde_json::from_slice(&json)?;
360        projection.apply(&event)?;
361        ids.insert(id, json);
362    }
363    Ok((ids, projection))
364}
365
366impl Projection {
367    fn apply(&mut self, event: &Event) -> Result<(), StoreError> {
368        match event {
369            Event::RegisterSegment(registration) => self.register(registration),
370            Event::RecordAttempt(attempt) => self.record_attempt(attempt),
371            Event::SelectAttempt(selection) => self.select(selection),
372            Event::SetSampleState(change) => self.set_sample_state(change),
373            Event::PutArtifact(artifact) => self.put_artifact(artifact),
374            Event::SetActiveModel(change) => self.set_active_model(change),
375        }
376    }
377
378    fn register(&mut self, registration: &SegmentRegistration) -> Result<(), StoreError> {
379        registration
380            .segment
381            .validate()
382            .map_err(|e| StoreError::Validation(e.to_string()))?;
383        let segment = &registration.segment;
384        let duration = segment.end_ms - segment.start_ms;
385        if duration == 0 || duration >= 240_000 {
386            return validation("segment duration must be 1..239999 ms");
387        }
388        if segment.ordinal == 0 && segment.start_ms != 0 {
389            return validation("source ordinal zero must begin at zero");
390        }
391        if self.registrations.contains_key(&segment.clip_object) {
392            return conflict("clip object is already registered");
393        }
394        for old in self
395            .registrations
396            .values()
397            .filter(|old| old.segment.source_object == segment.source_object)
398        {
399            if old.segment.policy != segment.policy
400                || old.segment.segment_count != segment.segment_count
401                || old.group_id != registration.group_id
402                || old.recording_kind != registration.recording_kind
403            {
404                return conflict("source plan, group, or recording kind differs");
405            }
406            if old.segment.ordinal == segment.ordinal {
407                return conflict("source ordinal is already registered");
408            }
409            let old_duration = old.segment.end_ms - old.segment.start_ms;
410            if old_duration.abs_diff(duration) > 1 {
411                return validation("source segment lengths differ by more than one millisecond");
412            }
413            let ordered = if old.segment.ordinal < segment.ordinal {
414                old.segment.start_ms < segment.start_ms && old.segment.end_ms < segment.end_ms
415            } else {
416                segment.start_ms < old.segment.start_ms && segment.end_ms < old.segment.end_ms
417            };
418            if !ordered {
419                return validation("source segment ranges do not follow ordinal order");
420            }
421            if old.segment.ordinal.checked_add(1) == Some(segment.ordinal)
422                && old.segment.end_ms.checked_sub(segment.start_ms) != Some(5_000)
423            {
424                return validation("adjacent source segments must overlap by 5000 ms");
425            }
426            if segment.ordinal.checked_add(1) == Some(old.segment.ordinal)
427                && segment.end_ms.checked_sub(old.segment.start_ms) != Some(5_000)
428            {
429                return validation("adjacent source segments must overlap by 5000 ms");
430            }
431        }
432        self.registrations
433            .insert(segment.clip_object.clone(), registration.clone());
434        Ok(())
435    }
436
437    fn record_attempt(&mut self, attempt: &AttemptRecord) -> Result<(), StoreError> {
438        if self.attempts.contains_key(&attempt.attempt_id) {
439            return conflict("attempt ID is already recorded");
440        }
441        let registration = self
442            .registrations
443            .get(&attempt.clip_object)
444            .ok_or_else(|| StoreError::Conflict("clip must be registered first".into()))?;
445        if !matches!(
446            attempt.outcome,
447            StoredAttemptOutcome::ProviderFailure { .. }
448        ) && attempt.provider_result_object.is_none()
449        {
450            return validation("response-bearing attempt needs a result object");
451        }
452        match &attempt.outcome {
453            StoredAttemptOutcome::Scored {
454                recording_quality,
455                speakers,
456            } => {
457                if *recording_quality > 100 || speakers.is_empty() {
458                    return validation("scored attempt needs quality <=100 and speakers");
459                }
460                let duration = registration.segment.end_ms - registration.segment.start_ms;
461                for (index, speaker) in speakers.iter().enumerate() {
462                    if usize::from(speaker.speaker_ordinal) != index {
463                        return validation("speaker ordinals must be contiguous from zero");
464                    }
465                    bounded(&speaker.closest_dialect, 128, "dialect")?;
466                    if speaker.usable_speech_ms == 0
467                        || u64::from(speaker.usable_speech_ms) > duration
468                    {
469                        return validation("usable speech is outside segment duration");
470                    }
471                    sample_id(&attempt.attempt_id, speaker.speaker_ordinal)?;
472                }
473            }
474            StoredAttemptOutcome::Unscorable { reason } => bounded(reason, 240, "reason")?,
475            StoredAttemptOutcome::InvalidResponse { error } => bounded(error, 240, "error")?,
476            StoredAttemptOutcome::ProviderFailure { detail, .. } => bounded(detail, 240, "detail")?,
477        }
478        self.attempts
479            .insert(attempt.attempt_id.clone(), attempt.clone());
480        Ok(())
481    }
482
483    fn select(&mut self, selection: &AttemptSelection) -> Result<(), StoreError> {
484        bounded(&selection.reason, 240, "selection reason")?;
485        let attempt = self
486            .attempts
487            .get(&selection.attempt_id)
488            .ok_or_else(|| StoreError::Conflict("selected attempt does not exist".into()))?;
489        if attempt.clip_object != selection.clip_object
490            || attempt.extraction_key != selection.extraction_key
491            || !matches!(attempt.outcome, StoredAttemptOutcome::Scored { .. })
492        {
493            return conflict("selection does not match a scored clip attempt");
494        }
495        self.selections.insert(
496            (
497                selection.clip_object.clone(),
498                selection.extraction_key.clone(),
499            ),
500            selection.clone(),
501        );
502        Ok(())
503    }
504
505    fn set_sample_state(&mut self, change: &SampleStateChange) -> Result<(), StoreError> {
506        bounded(&change.reason, 240, "sample-state reason")?;
507        if self.sample_owner(&change.sample_id).is_none() {
508            return conflict("sample ID does not exist");
509        }
510        self.states.insert(change.sample_id.clone(), change.clone());
511        Ok(())
512    }
513
514    fn put_artifact(&mut self, artifact: &ArtifactRecord) -> Result<(), StoreError> {
515        let actual: [u8; 32] = Sha256::digest(&artifact.bytes).into();
516        if actual != artifact.sha256 {
517            return validation("artifact SHA-256 does not match bytes");
518        }
519        if let Some(old) = self.artifacts.get(&artifact.artifact_id) {
520            if old != artifact {
521                return conflict("artifact ID is immutable");
522            }
523            return Ok(());
524        }
525        self.artifacts
526            .insert(artifact.artifact_id.clone(), artifact.clone());
527        Ok(())
528    }
529
530    fn set_active_model(&mut self, change: &ActiveModelChange) -> Result<(), StoreError> {
531        bounded(&change.reason, 240, "activation reason")?;
532        if let Some(id) = &change.model_artifact_id {
533            let artifact = self
534                .artifacts
535                .get(id)
536                .ok_or_else(|| StoreError::Conflict("model artifact does not exist".into()))?;
537            if artifact.kind != ArtifactKind::Model || artifact.cohort_id != change.cohort_id {
538                return conflict("active artifact must be a Model in the same cohort");
539            }
540        }
541        self.active_models
542            .insert(change.cohort_id.clone(), change.clone());
543        Ok(())
544    }
545
546    fn sample_owner(&self, wanted: &Key) -> Option<(&AttemptRecord, &StoredSpeaker, u8)> {
547        for attempt in self.attempts.values() {
548            if let StoredAttemptOutcome::Scored {
549                recording_quality,
550                speakers,
551            } = &attempt.outcome
552            {
553                for speaker in speakers {
554                    if sample_id(&attempt.attempt_id, speaker.speaker_ordinal)
555                        .ok()
556                        .as_ref()
557                        == Some(wanted)
558                    {
559                        return Some((attempt, speaker, *recording_quality));
560                    }
561                }
562            }
563        }
564        None
565    }
566
567    fn snapshot(&self, attempt: &AttemptRecord) -> Result<AttemptSnapshot, StoreError> {
568        let sample_ids = match &attempt.outcome {
569            StoredAttemptOutcome::Scored { speakers, .. } => speakers
570                .iter()
571                .map(|speaker| sample_id(&attempt.attempt_id, speaker.speaker_ordinal))
572                .collect::<Result<_, _>>()?,
573            _ => Vec::new(),
574        };
575        let selected = self
576            .selections
577            .get(&(attempt.clip_object.clone(), attempt.extraction_key.clone()))
578            .is_some_and(|selection| selection.attempt_id == attempt.attempt_id);
579        Ok(AttemptSnapshot {
580            attempt: attempt.clone(),
581            sample_ids,
582            selected,
583        })
584    }
585
586    fn rows(&self, cohort_id: &Key, selected_only: bool) -> Result<Vec<LabeledSample>, StoreError> {
587        let mut rows = Vec::new();
588        for attempt in self
589            .attempts
590            .values()
591            .filter(|attempt| &attempt.extraction_key == cohort_id)
592        {
593            if selected_only
594                && !self
595                    .selections
596                    .get(&(attempt.clip_object.clone(), attempt.extraction_key.clone()))
597                    .is_some_and(|selection| selection.attempt_id == attempt.attempt_id)
598            {
599                continue;
600            }
601            let StoredAttemptOutcome::Scored {
602                recording_quality,
603                speakers,
604            } = &attempt.outcome
605            else {
606                continue;
607            };
608            let registration = self
609                .registrations
610                .get(&attempt.clip_object)
611                .ok_or_else(|| StoreError::Storage("attempt lost its registration".into()))?;
612            for speaker in speakers {
613                let id = sample_id(&attempt.attempt_id, speaker.speaker_ordinal)?;
614                let Some(SampleStateChange {
615                    state: SampleState::Confirmed { speaker_id },
616                    ..
617                }) = self.states.get(&id)
618                else {
619                    continue;
620                };
621                rows.push(LabeledSample {
622                    sample_id: id,
623                    attempt_id: attempt.attempt_id.clone(),
624                    speaker_id: speaker_id.clone(),
625                    cohort_id: attempt.extraction_key.clone(),
626                    group_id: registration.group_id.clone(),
627                    clip_object: attempt.clip_object.clone(),
628                    primary_language: speaker.primary_language.clone(),
629                    recording_kind: registration.recording_kind,
630                    usable_speech_ms: speaker.usable_speech_ms,
631                    recording_quality: *recording_quality,
632                    features: speaker.features.clone(),
633                });
634            }
635        }
636        Ok(rows)
637    }
638
639    fn read(&self, query: Query) -> Result<ResponseKind, StoreError> {
640        match query {
641            Query::Attempt { attempt_id } => Ok(ResponseKind::Attempt(
642                self.attempts
643                    .get(&attempt_id)
644                    .map(|attempt| self.snapshot(attempt))
645                    .transpose()?,
646            )),
647            Query::Attempts {
648                extraction_key,
649                source_object,
650            } => Ok(ResponseKind::Attempts(
651                self.attempts
652                    .values()
653                    .filter(|attempt| {
654                        extraction_key
655                            .as_ref()
656                            .is_none_or(|key| key == &attempt.extraction_key)
657                            && source_object.as_ref().is_none_or(|source| {
658                                self.registrations.get(&attempt.clip_object).is_some_and(
659                                    |registration| &registration.segment.source_object == source,
660                                )
661                            })
662                    })
663                    .map(|attempt| self.snapshot(attempt))
664                    .collect::<Result<_, _>>()?,
665            )),
666            Query::TrainingRows { cohort_id } => {
667                Ok(ResponseKind::TrainingRows(self.rows(&cohort_id, true)?))
668            }
669            Query::RepeatabilityRows { cohort_id } => Ok(ResponseKind::RepeatabilityRows(
670                self.rows(&cohort_id, false)?,
671            )),
672            Query::Artifact { artifact_id } => Ok(ResponseKind::Artifact(
673                self.artifacts.get(&artifact_id).cloned(),
674            )),
675            Query::ActiveModel { cohort_id } => Ok(ResponseKind::ActiveModel(
676                self.active_models
677                    .get(&cohort_id)
678                    .and_then(|change| change.model_artifact_id.clone()),
679            )),
680            Query::Inventory { extraction_key } => {
681                Ok(ResponseKind::Inventory(self.inventory(extraction_key)?))
682            }
683        }
684    }
685
686    fn inventory(&self, filter: Option<Key>) -> Result<InventorySnapshot, StoreError> {
687        let included = |attempt: &&AttemptRecord| {
688            filter
689                .as_ref()
690                .is_none_or(|key| key == &attempt.extraction_key)
691        };
692        let attempts = self
693            .attempts
694            .values()
695            .filter(included)
696            .map(|attempt| self.snapshot(attempt))
697            .collect::<Result<Vec<_>, _>>()?;
698        let included_samples: BTreeSet<Key> = attempts
699            .iter()
700            .flat_map(|attempt| attempt.sample_ids.iter().cloned())
701            .collect();
702        let selections = self
703            .selections
704            .values()
705            .filter(|selection| {
706                filter
707                    .as_ref()
708                    .is_none_or(|key| key == &selection.extraction_key)
709            })
710            .cloned()
711            .map(|selection| SelectionSnapshot { selection })
712            .collect();
713        let sample_states = self
714            .states
715            .values()
716            .filter(|change| filter.is_none() || included_samples.contains(&change.sample_id))
717            .cloned()
718            .map(|change| SampleStateSnapshot { change })
719            .collect();
720        let mut registrations: Vec<_> = self.registrations.values().cloned().collect();
721        registrations.sort_by(|a, b| {
722            (&a.segment.source_object, a.segment.ordinal)
723                .cmp(&(&b.segment.source_object, b.segment.ordinal))
724        });
725        let mut sources: BTreeMap<ObjectId, (u16, BTreeSet<u16>)> = BTreeMap::new();
726        for registration in &registrations {
727            let entry = sources
728                .entry(registration.segment.source_object.clone())
729                .or_insert((registration.segment.segment_count, BTreeSet::new()));
730            entry.1.insert(registration.segment.ordinal);
731        }
732        let missing_segment_ordinals = sources
733            .into_iter()
734            .filter_map(|(source_object, (count, present))| {
735                let ordinals: Vec<_> = (0..count)
736                    .filter(|ordinal| !present.contains(ordinal))
737                    .collect();
738                (!ordinals.is_empty()).then_some(MissingSegmentOrdinals {
739                    source_object,
740                    ordinals,
741                })
742            })
743            .collect();
744        let mut outcome_counts = OutcomeCounts::default();
745        for attempt in self.attempts.values().filter(included) {
746            match attempt.outcome {
747                StoredAttemptOutcome::Scored { .. } => outcome_counts.scored += 1,
748                StoredAttemptOutcome::Unscorable { .. } => outcome_counts.unscorable += 1,
749                StoredAttemptOutcome::InvalidResponse { .. } => {
750                    outcome_counts.invalid_response += 1
751                }
752                StoredAttemptOutcome::ProviderFailure { .. } => {
753                    outcome_counts.provider_failure += 1
754                }
755            }
756        }
757        Ok(InventorySnapshot {
758            registrations,
759            attempts,
760            selections,
761            sample_states,
762            artifacts: self
763                .artifacts
764                .values()
765                .map(|artifact| ArtifactMetadata {
766                    artifact_id: artifact.artifact_id.clone(),
767                    cohort_id: artifact.cohort_id.clone(),
768                    kind: artifact.kind,
769                    sha256: artifact.sha256,
770                    byte_len: artifact.bytes.len() as u64,
771                })
772                .collect(),
773            active_models: self
774                .active_models
775                .values()
776                .cloned()
777                .map(|change| ActiveModelSnapshot { change })
778                .collect(),
779            missing_segment_ordinals,
780            outcome_counts,
781        })
782    }
783}
784
785fn sample_id(attempt_id: &Key, ordinal: u16) -> Result<Key, StoreError> {
786    let mut digest = Sha256::new();
787    digest.update(attempt_id.as_ref().as_bytes());
788    digest.update([0]);
789    digest.update(ordinal.to_be_bytes());
790    let hex = digest
791        .finalize()
792        .iter()
793        .map(|byte| format!("{byte:02x}"))
794        .collect::<String>();
795    Key::parse(&format!("sample:sha256:{hex}"))
796        .map_err(|e| StoreError::Storage(format!("generated invalid sample ID: {e}")))
797}
798
799fn bounded(value: &str, maximum: usize, name: &str) -> Result<(), StoreError> {
800    if value.trim().is_empty() || value.len() > maximum {
801        return validation(format!(
802            "{name} must be nonblank and at most {maximum} bytes"
803        ));
804    }
805    Ok(())
806}
807
808fn validation<T>(message: impl Into<String>) -> Result<T, StoreError> {
809    Err(StoreError::Validation(message.into()))
810}
811
812fn conflict<T>(message: impl Into<String>) -> Result<T, StoreError> {
813    Err(StoreError::Conflict(message.into()))
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819    use std::{
820        fs,
821        path::PathBuf,
822        sync::{
823            Arc,
824            atomic::{AtomicU64, Ordering},
825        },
826        thread,
827    };
828
829    static NEXT_PATH: AtomicU64 = AtomicU64::new(0);
830
831    fn path() -> PathBuf {
832        std::env::temp_dir().join(format!(
833            "kcode-speaker-store-{}-{}.db",
834            std::process::id(),
835            NEXT_PATH.fetch_add(1, Ordering::Relaxed)
836        ))
837    }
838
839    fn key(value: &str) -> Key {
840        Key::parse(value).unwrap()
841    }
842
843    fn object(value: &str) -> ObjectId {
844        ObjectId::parse(value).unwrap()
845    }
846
847    fn features(seed: u8) -> FeatureVector {
848        FeatureVector::new([seed; 35]).unwrap()
849    }
850
851    fn registration(source: &str, clip: &str, ordinal: u16, count: u16) -> SegmentRegistration {
852        SegmentRegistration {
853            segment: SegmentRef {
854                source_object: object(source),
855                clip_object: object(clip),
856                ordinal,
857                segment_count: count,
858                start_ms: u64::from(ordinal) * 95_000,
859                end_ms: u64::from(ordinal) * 95_000 + 100_000,
860                policy: key("speaker-segments/1"),
861            },
862            group_id: key("group/1"),
863            recording_kind: RecordingKind::VoiceNote,
864        }
865    }
866
867    fn scored(id: &str, clip: &str, extraction: &str, speakers: u16) -> AttemptRecord {
868        AttemptRecord {
869            attempt_id: key(id),
870            clip_object: object(clip),
871            extraction_key: key(extraction),
872            provider_result_object: Some(object("RESULT01")),
873            outcome: StoredAttemptOutcome::Scored {
874                recording_quality: 80,
875                speakers: (0..speakers)
876                    .map(|speaker_ordinal| StoredSpeaker {
877                        speaker_ordinal,
878                        primary_language: key("en"),
879                        closest_dialect: "General American".into(),
880                        usable_speech_ms: 20_000,
881                        features: features(50),
882                    })
883                    .collect(),
884            },
885        }
886    }
887
888    fn failure(id: &str, clip: &str, extraction: &str) -> AttemptRecord {
889        AttemptRecord {
890            attempt_id: key(id),
891            clip_object: object(clip),
892            extraction_key: key(extraction),
893            provider_result_object: None,
894            outcome: StoredAttemptOutcome::ProviderFailure {
895                code: key("timeout"),
896                detail: "provider timed out".into(),
897            },
898        }
899    }
900
901    fn event(id: &str, event: Event) -> EventEnvelope {
902        EventEnvelope {
903            event_id: key(id),
904            event,
905        }
906    }
907
908    fn commit(store: &Store, events: Vec<EventEnvelope>) -> Response {
909        store.execute(Request::Commit(events)).unwrap()
910    }
911
912    fn attempt(store: &Store, id: &str) -> AttemptSnapshot {
913        let response = store
914            .execute(Request::Read(Query::Attempt {
915                attempt_id: key(id),
916            }))
917            .unwrap();
918        let ResponseKind::Attempt(Some(attempt)) = response.result else {
919            panic!("missing attempt")
920        };
921        attempt
922    }
923
924    fn selection(id: &str, clip: &str, extraction: &str) -> AttemptSelection {
925        AttemptSelection {
926            clip_object: object(clip),
927            extraction_key: key(extraction),
928            attempt_id: key(id),
929            reason: "reviewed".into(),
930        }
931    }
932
933    fn state(sample_id: Key, state: SampleState) -> SampleStateChange {
934        SampleStateChange {
935            sample_id,
936            state,
937            reason: "human review".into(),
938        }
939    }
940
941    #[test]
942    fn fresh_database_restarts() {
943        let path = path();
944        {
945            let store = open(&path).unwrap();
946            let response = commit(
947                &store,
948                vec![event(
949                    "event/1",
950                    Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
951                )],
952            );
953            assert_eq!(response.revision, 1);
954        }
955        let store = open(&path).unwrap();
956        let response = store
957            .execute(Request::Read(Query::Inventory {
958                extraction_key: None,
959            }))
960            .unwrap();
961        assert_eq!(response.revision, 1);
962        let ResponseKind::Inventory(inventory) = response.result else {
963            panic!()
964        };
965        assert_eq!(inventory.registrations.len(), 1);
966        drop(store);
967        fs::remove_file(path).unwrap();
968    }
969
970    #[test]
971    fn commit_is_atomic_and_event_ids_are_idempotent() {
972        let path = path();
973        let store = open(&path).unwrap();
974        let registration_event = event(
975            "event/register",
976            Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
977        );
978        let bad = event(
979            "event/bad",
980            Event::RecordAttempt(scored("attempt/1", "UNKNOWN1", "extract/1", 1)),
981        );
982        assert!(
983            store
984                .execute(Request::Commit(vec![registration_event.clone(), bad]))
985                .is_err()
986        );
987        let response = store
988            .execute(Request::Read(Query::Inventory {
989                extraction_key: None,
990            }))
991            .unwrap();
992        assert_eq!(response.revision, 0);
993
994        assert!(matches!(
995            commit(&store, vec![registration_event.clone()]).result,
996            ResponseKind::Commit { applied: 1 }
997        ));
998        assert!(matches!(
999            commit(
1000                &store,
1001                vec![registration_event.clone(), registration_event.clone()]
1002            )
1003            .result,
1004            ResponseKind::Commit { applied: 0 }
1005        ));
1006        let mut changed = registration_event;
1007        changed.event = Event::RegisterSegment(registration("SOURCE02", "CLIP0002", 0, 1));
1008        assert!(matches!(
1009            store.execute(Request::Commit(vec![changed])),
1010            Err(StoreError::Conflict(_))
1011        ));
1012        assert_eq!(
1013            store
1014                .execute(Request::Read(Query::Inventory {
1015                    extraction_key: None
1016                }))
1017                .unwrap()
1018                .revision,
1019            1
1020        );
1021        drop(store);
1022        fs::remove_file(path).unwrap();
1023    }
1024
1025    #[test]
1026    fn registration_order_and_source_plan_conflicts_fail() {
1027        let path = path();
1028        let store = open(&path).unwrap();
1029        assert!(matches!(
1030            store.execute(Request::Commit(vec![event(
1031                "attempt/early",
1032                Event::RecordAttempt(scored("attempt/1", "CLIP0001", "extract/1", 1))
1033            )])),
1034            Err(StoreError::Conflict(_))
1035        ));
1036        commit(
1037            &store,
1038            vec![event(
1039                "register/0",
1040                Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 2)),
1041            )],
1042        );
1043        let mut wrong_count = registration("SOURCE01", "CLIP0002", 1, 3);
1044        assert!(
1045            store
1046                .execute(Request::Commit(vec![event(
1047                    "register/wrong-count",
1048                    Event::RegisterSegment(wrong_count.clone())
1049                )]))
1050                .is_err()
1051        );
1052        wrong_count.segment.segment_count = 2;
1053        wrong_count.segment.policy = key("different-plan");
1054        assert!(
1055            store
1056                .execute(Request::Commit(vec![event(
1057                    "register/wrong-plan",
1058                    Event::RegisterSegment(wrong_count)
1059                )]))
1060                .is_err()
1061        );
1062        let duplicate_ordinal = registration("SOURCE01", "CLIP0003", 0, 2);
1063        assert!(
1064            store
1065                .execute(Request::Commit(vec![event(
1066                    "register/duplicate",
1067                    Event::RegisterSegment(duplicate_ordinal)
1068                )]))
1069                .is_err()
1070        );
1071        drop(store);
1072        fs::remove_file(path).unwrap();
1073    }
1074
1075    #[test]
1076    fn retries_replacement_ids_and_label_projections() {
1077        let path = path();
1078        let store = open(&path).unwrap();
1079        commit(
1080            &store,
1081            vec![
1082                event(
1083                    "register/1",
1084                    Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
1085                ),
1086                event(
1087                    "attempt/1",
1088                    Event::RecordAttempt(scored("try/one", "CLIP0001", "extract/1", 2)),
1089                ),
1090                event(
1091                    "attempt/2",
1092                    Event::RecordAttempt(scored("try/two", "CLIP0001", "extract/1", 1)),
1093                ),
1094                event(
1095                    "select/1",
1096                    Event::SelectAttempt(selection("try/one", "CLIP0001", "extract/1")),
1097                ),
1098            ],
1099        );
1100        let first = attempt(&store, "try/one");
1101        assert_eq!(first.sample_ids.len(), 2);
1102        assert_ne!(first.sample_ids[0], first.sample_ids[1]);
1103        let mut digest = Sha256::new();
1104        digest.update(b"try/one");
1105        digest.update([0]);
1106        digest.update(0u16.to_be_bytes());
1107        let expected = key(&format!("sample:sha256:{:x}", digest.finalize()));
1108        assert_eq!(first.sample_ids[0], expected);
1109
1110        let second = attempt(&store, "try/two");
1111        commit(
1112            &store,
1113            vec![
1114                event(
1115                    "state/1",
1116                    Event::SetSampleState(state(
1117                        first.sample_ids[0].clone(),
1118                        SampleState::Confirmed {
1119                            speaker_id: key("speaker/alice"),
1120                        },
1121                    )),
1122                ),
1123                event(
1124                    "state/2",
1125                    Event::SetSampleState(state(
1126                        second.sample_ids[0].clone(),
1127                        SampleState::Confirmed {
1128                            speaker_id: key("speaker/alice"),
1129                        },
1130                    )),
1131                ),
1132            ],
1133        );
1134        let training = store
1135            .execute(Request::Read(Query::TrainingRows {
1136                cohort_id: key("extract/1"),
1137            }))
1138            .unwrap();
1139        let ResponseKind::TrainingRows(rows) = training.result else {
1140            panic!()
1141        };
1142        assert_eq!(rows.len(), 1);
1143        assert_eq!(rows[0].attempt_id, key("try/one"));
1144        let repeat = store
1145            .execute(Request::Read(Query::RepeatabilityRows {
1146                cohort_id: key("extract/1"),
1147            }))
1148            .unwrap();
1149        let ResponseKind::RepeatabilityRows(rows) = repeat.result else {
1150            panic!()
1151        };
1152        assert_eq!(rows.len(), 2);
1153
1154        commit(
1155            &store,
1156            vec![event(
1157                "select/2",
1158                Event::SelectAttempt(selection("try/two", "CLIP0001", "extract/1")),
1159            )],
1160        );
1161        let ResponseKind::TrainingRows(rows) = store
1162            .execute(Request::Read(Query::TrainingRows {
1163                cohort_id: key("extract/1"),
1164            }))
1165            .unwrap()
1166            .result
1167        else {
1168            panic!()
1169        };
1170        assert_eq!(rows[0].attempt_id, key("try/two"));
1171
1172        commit(
1173            &store,
1174            vec![event(
1175                "state/correct",
1176                Event::SetSampleState(state(
1177                    second.sample_ids[0].clone(),
1178                    SampleState::Confirmed {
1179                        speaker_id: key("speaker/bob"),
1180                    },
1181                )),
1182            )],
1183        );
1184        let ResponseKind::TrainingRows(rows) = store
1185            .execute(Request::Read(Query::TrainingRows {
1186                cohort_id: key("extract/1"),
1187            }))
1188            .unwrap()
1189            .result
1190        else {
1191            panic!()
1192        };
1193        assert_eq!(rows[0].speaker_id, key("speaker/bob"));
1194        commit(
1195            &store,
1196            vec![event(
1197                "state/retract",
1198                Event::SetSampleState(state(second.sample_ids[0].clone(), SampleState::Retracted)),
1199            )],
1200        );
1201        let ResponseKind::TrainingRows(rows) = store
1202            .execute(Request::Read(Query::TrainingRows {
1203                cohort_id: key("extract/1"),
1204            }))
1205            .unwrap()
1206            .result
1207        else {
1208            panic!()
1209        };
1210        assert!(rows.is_empty());
1211        drop(store);
1212        fs::remove_file(path).unwrap();
1213    }
1214
1215    #[test]
1216    fn artifacts_are_hashed_immutable_and_cohort_checked() {
1217        let path = path();
1218        let store = open(&path).unwrap();
1219        let bytes = b"model bytes".to_vec();
1220        let hash: [u8; 32] = Sha256::digest(&bytes).into();
1221        let model = ArtifactRecord {
1222            artifact_id: key("artifact/model"),
1223            cohort_id: key("cohort/1"),
1224            kind: ArtifactKind::Model,
1225            sha256: hash,
1226            bytes,
1227        };
1228        let evaluation = ArtifactRecord {
1229            artifact_id: key("artifact/eval"),
1230            cohort_id: key("cohort/1"),
1231            kind: ArtifactKind::Evaluation,
1232            sha256: Sha256::digest(b"evaluation").into(),
1233            bytes: b"evaluation".to_vec(),
1234        };
1235        commit(
1236            &store,
1237            vec![
1238                event("artifact/model", Event::PutArtifact(model.clone())),
1239                event("artifact/eval", Event::PutArtifact(evaluation)),
1240            ],
1241        );
1242        let activate = |id: &str, cohort: &str, artifact: &str| {
1243            event(
1244                id,
1245                Event::SetActiveModel(ActiveModelChange {
1246                    cohort_id: key(cohort),
1247                    model_artifact_id: Some(key(artifact)),
1248                    reason: "approved".into(),
1249                }),
1250            )
1251        };
1252        assert!(
1253            store
1254                .execute(Request::Commit(vec![activate(
1255                    "activate/wrong-kind",
1256                    "cohort/1",
1257                    "artifact/eval"
1258                )]))
1259                .is_err()
1260        );
1261        assert!(
1262            store
1263                .execute(Request::Commit(vec![activate(
1264                    "activate/wrong-cohort",
1265                    "cohort/2",
1266                    "artifact/model"
1267                )]))
1268                .is_err()
1269        );
1270
1271        let mut changed = model.clone();
1272        changed.bytes.push(1);
1273        changed.sha256 = Sha256::digest(&changed.bytes).into();
1274        assert!(matches!(
1275            store.execute(Request::Commit(vec![event(
1276                "artifact/change",
1277                Event::PutArtifact(changed)
1278            )])),
1279            Err(StoreError::Conflict(_))
1280        ));
1281        let mut bad_hash = model;
1282        bad_hash.artifact_id = key("artifact/bad");
1283        bad_hash.sha256 = [0; 32];
1284        assert!(matches!(
1285            store.execute(Request::Commit(vec![event(
1286                "artifact/bad",
1287                Event::PutArtifact(bad_hash)
1288            )])),
1289            Err(StoreError::Validation(_))
1290        ));
1291        commit(
1292            &store,
1293            vec![activate("activate/good", "cohort/1", "artifact/model")],
1294        );
1295        let ResponseKind::ActiveModel(Some(active)) = store
1296            .execute(Request::Read(Query::ActiveModel {
1297                cohort_id: key("cohort/1"),
1298            }))
1299            .unwrap()
1300            .result
1301        else {
1302            panic!()
1303        };
1304        assert_eq!(active, key("artifact/model"));
1305        commit(
1306            &store,
1307            vec![event(
1308                "activate/none",
1309                Event::SetActiveModel(ActiveModelChange {
1310                    cohort_id: key("cohort/1"),
1311                    model_artifact_id: None,
1312                    reason: "retired".into(),
1313                }),
1314            )],
1315        );
1316        assert!(matches!(
1317            store
1318                .execute(Request::Read(Query::ActiveModel {
1319                    cohort_id: key("cohort/1")
1320                }))
1321                .unwrap()
1322                .result,
1323            ResponseKind::ActiveModel(None)
1324        ));
1325        drop(store);
1326        fs::remove_file(path).unwrap();
1327    }
1328
1329    #[test]
1330    fn filtering_inventory_missing_and_counts_are_typed() {
1331        let path = path();
1332        let store = open(&path).unwrap();
1333        let unscorable = AttemptRecord {
1334            attempt_id: key("unscorable"),
1335            clip_object: object("CLIP0001"),
1336            extraction_key: key("extract/1"),
1337            provider_result_object: Some(object("RESULT02")),
1338            outcome: StoredAttemptOutcome::Unscorable {
1339                reason: "too faint".into(),
1340            },
1341        };
1342        let invalid = AttemptRecord {
1343            attempt_id: key("invalid"),
1344            clip_object: object("CLIP0001"),
1345            extraction_key: key("extract/1"),
1346            provider_result_object: Some(object("RESULT03")),
1347            outcome: StoredAttemptOutcome::InvalidResponse {
1348                error: "extra field".into(),
1349            },
1350        };
1351        commit(
1352            &store,
1353            vec![
1354                event(
1355                    "register",
1356                    Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 2)),
1357                ),
1358                event(
1359                    "attempt/scored",
1360                    Event::RecordAttempt(scored("scored", "CLIP0001", "extract/1", 1)),
1361                ),
1362                event("attempt/unscorable", Event::RecordAttempt(unscorable)),
1363                event("attempt/invalid", Event::RecordAttempt(invalid)),
1364                event(
1365                    "attempt/failure",
1366                    Event::RecordAttempt(failure("failure", "CLIP0001", "extract/1")),
1367                ),
1368                event(
1369                    "attempt/other",
1370                    Event::RecordAttempt(failure("other", "CLIP0001", "extract/2")),
1371                ),
1372            ],
1373        );
1374        let ResponseKind::Attempts(filtered) = store
1375            .execute(Request::Read(Query::Attempts {
1376                extraction_key: Some(key("extract/1")),
1377                source_object: Some(object("SOURCE01")),
1378            }))
1379            .unwrap()
1380            .result
1381        else {
1382            panic!()
1383        };
1384        assert_eq!(filtered.len(), 4);
1385        let ResponseKind::Inventory(inventory) = store
1386            .execute(Request::Read(Query::Inventory {
1387                extraction_key: Some(key("extract/1")),
1388            }))
1389            .unwrap()
1390            .result
1391        else {
1392            panic!()
1393        };
1394        assert_eq!(inventory.attempts.len(), 4);
1395        assert_eq!(
1396            inventory.outcome_counts,
1397            OutcomeCounts {
1398                scored: 1,
1399                unscorable: 1,
1400                invalid_response: 1,
1401                provider_failure: 1,
1402            }
1403        );
1404        assert_eq!(inventory.missing_segment_ordinals.len(), 1);
1405        assert_eq!(inventory.missing_segment_ordinals[0].ordinals, vec![1]);
1406        drop(store);
1407        fs::remove_file(path).unwrap();
1408    }
1409
1410    #[test]
1411    fn future_schema_is_rejected() {
1412        let path = path();
1413        let connection = Connection::open(&path).unwrap();
1414        connection
1415            .execute_batch("PRAGMA user_version = 2;")
1416            .unwrap();
1417        drop(connection);
1418        assert!(matches!(open(&path), Err(StoreError::SchemaVersion(2))));
1419        fs::remove_file(path).unwrap();
1420    }
1421
1422    #[test]
1423    fn one_store_serializes_concurrent_calls() {
1424        let path = path();
1425        let store = Arc::new(open(&path).unwrap());
1426        commit(
1427            &store,
1428            vec![event(
1429                "register",
1430                Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
1431            )],
1432        );
1433        let threads: Vec<_> = (0..8)
1434            .map(|index| {
1435                let store = Arc::clone(&store);
1436                thread::spawn(move || {
1437                    commit(
1438                        &store,
1439                        vec![event(
1440                            &format!("event/{index}"),
1441                            Event::RecordAttempt(failure(
1442                                &format!("attempt/{index}"),
1443                                "CLIP0001",
1444                                "extract/1",
1445                            )),
1446                        )],
1447                    );
1448                })
1449            })
1450            .collect();
1451        for thread in threads {
1452            thread.join().unwrap();
1453        }
1454        let response = store
1455            .execute(Request::Read(Query::Attempts {
1456                extraction_key: None,
1457                source_object: None,
1458            }))
1459            .unwrap();
1460        assert_eq!(response.revision, 9);
1461        let ResponseKind::Attempts(attempts) = response.result else {
1462            panic!()
1463        };
1464        assert_eq!(attempts.len(), 8);
1465        drop(store);
1466        fs::remove_file(path).unwrap();
1467    }
1468}