1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct Psp9LedgerRow {
8 pub session_id: String,
9 pub sequence: i64,
10 pub event_json: String,
11 pub prev_hash: String,
12 pub hash: String,
13}
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct Psp9VerdictRow {
17 pub session_id: String,
18 pub candidate_id: String,
19 pub validator_id: String,
20 pub stratum: String,
21 pub missed: bool,
22 pub unsafe_label: Option<bool>,
23 pub evidence_hash: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct Psp9CalibrationEpochRow {
28 pub epoch_id: String,
29 pub stratum: String,
30 pub target_rho: f64,
31 pub threshold: Option<f64>,
32 pub state: String,
33 pub sample_count: i64,
34}
35
36pub type Psp9SampleRow = (String, f64, Option<bool>, bool);
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct Psp9ExternalEffectRow {
41 pub idempotency_key: String,
42 pub intent_hash: String,
43 pub intent_json: String,
44 pub result_json: Option<String>,
45 pub status: String,
46}
47
48impl SessionStore {
49 pub fn put_psp9_artifact(
50 &self,
51 content_hash: &str,
52 content: &[u8],
53 media_type: &str,
54 ) -> Result<()> {
55 self.conn.lock().unwrap().execute(
56 "INSERT INTO psp9_artifacts (content_hash, content, byte_len, media_type) \
57 VALUES (?, ?, ?, ?) ON CONFLICT (content_hash) DO NOTHING",
58 duckdb::params![content_hash, content, content.len() as i64, media_type],
59 )?;
60 Ok(())
61 }
62
63 pub fn get_psp9_artifact(&self, content_hash: &str) -> Result<Option<Vec<u8>>> {
64 let conn = self.conn.lock().unwrap();
65 let mut statement =
66 conn.prepare("SELECT content FROM psp9_artifacts WHERE content_hash = ?")?;
67 let mut rows = statement.query([content_hash])?;
68 Ok(rows.next()?.map(|row| row.get(0)).transpose()?)
69 }
70
71 pub fn record_psp9_checkpoint(
72 &self,
73 session_id: &str,
74 covered_event_root: &str,
75 checkpoint_json: &str,
76 ) -> Result<()> {
77 self.conn.lock().unwrap().execute(
78 "INSERT INTO psp9_context_checkpoints \
79 (session_id, covered_event_root, checkpoint_json) VALUES (?, ?, ?) \
80 ON CONFLICT (session_id, covered_event_root) DO NOTHING",
81 duckdb::params![session_id, covered_event_root, checkpoint_json],
82 )?;
83 Ok(())
84 }
85
86 pub fn latest_psp9_checkpoint(&self, session_id: &str) -> Result<Option<String>> {
88 let conn = self.conn.lock().unwrap();
89 let mut statement = conn.prepare(
90 "SELECT checkpoint_json FROM psp9_context_checkpoints WHERE session_id = ? \
91 ORDER BY created_at DESC, covered_event_root DESC LIMIT 1",
92 )?;
93 let mut rows = statement.query([session_id])?;
94 Ok(rows.next()?.map(|row| row.get(0)).transpose()?)
95 }
96
97 pub fn record_psp9_verdict(&self, row: &Psp9VerdictRow) -> Result<()> {
98 self.conn.lock().unwrap().execute(
99 "INSERT INTO psp9_verdicts (session_id, candidate_id, validator_id, stratum, \
100 missed, unsafe_label, evidence_hash) VALUES (?, ?, ?, ?, ?, ?, ?) \
101 ON CONFLICT (session_id, candidate_id, validator_id) DO UPDATE SET \
102 unsafe_label = excluded.unsafe_label, evidence_hash = excluded.evidence_hash",
103 duckdb::params![
104 row.session_id,
105 row.candidate_id,
106 row.validator_id,
107 row.stratum,
108 row.missed,
109 row.unsafe_label,
110 row.evidence_hash
111 ],
112 )?;
113 Ok(())
114 }
115
116 pub fn get_psp9_verdicts(&self, session_id: &str) -> Result<Vec<Psp9VerdictRow>> {
118 let conn = self.conn.lock().unwrap();
119 let mut statement = conn.prepare(
120 "SELECT session_id, candidate_id, validator_id, stratum, missed, \
121 unsafe_label, evidence_hash FROM psp9_verdicts WHERE session_id = ?",
122 )?;
123 let rows = statement.query_map([session_id], |row| {
124 Ok(Psp9VerdictRow {
125 session_id: row.get(0)?,
126 candidate_id: row.get(1)?,
127 validator_id: row.get(2)?,
128 stratum: row.get(3)?,
129 missed: row.get(4)?,
130 unsafe_label: row.get(5)?,
131 evidence_hash: row.get(6)?,
132 })
133 })?;
134 rows.collect::<std::result::Result<Vec<_>, _>>()
135 .map_err(Into::into)
136 }
137
138 pub fn record_psp9_calibration_epoch(&self, row: &Psp9CalibrationEpochRow) -> Result<()> {
139 self.conn.lock().unwrap().execute(
140 "INSERT INTO psp9_calibration_epochs \
141 (epoch_id, stratum, target_rho, threshold, state, sample_count) \
142 VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (epoch_id) DO NOTHING",
143 duckdb::params![
144 row.epoch_id,
145 row.stratum,
146 row.target_rho,
147 row.threshold,
148 row.state,
149 row.sample_count
150 ],
151 )?;
152 Ok(())
153 }
154
155 pub fn latest_psp9_calibration_epoch(
158 &self,
159 stratum: &str,
160 ) -> Result<Option<Psp9CalibrationEpochRow>> {
161 let conn = self.conn.lock().unwrap();
162 let mut statement = conn.prepare(
163 "SELECT epoch_id, stratum, target_rho, threshold, state, sample_count \
164 FROM psp9_calibration_epochs WHERE stratum = ? \
165 ORDER BY created_at DESC, epoch_id DESC LIMIT 1",
166 )?;
167 let mut rows = statement.query([stratum])?;
168 rows.next()?
169 .map(|row| {
170 Ok(Psp9CalibrationEpochRow {
171 epoch_id: row.get(0)?,
172 stratum: row.get(1)?,
173 target_rho: row.get(2)?,
174 threshold: row.get(3)?,
175 state: row.get(4)?,
176 sample_count: row.get(5)?,
177 })
178 })
179 .transpose()
180 }
181
182 pub fn psp9_calibration_samples(&self, epoch_id: &str) -> Result<Vec<Psp9SampleRow>> {
183 let conn = self.conn.lock().unwrap();
184 let mut statement = conn.prepare(
185 "SELECT sample_id, score, unsafe_label, audit_selected \
186 FROM psp9_calibration_samples WHERE epoch_id = ? ORDER BY created_at, sample_id",
187 )?;
188 let rows = statement.query_map([epoch_id], |row| {
189 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
190 })?;
191 rows.collect::<std::result::Result<Vec<_>, _>>()
192 .map_err(Into::into)
193 }
194
195 pub fn record_psp9_calibration_sample(
196 &self,
197 epoch_id: &str,
198 sample_id: &str,
199 score: f64,
200 unsafe_label: Option<bool>,
201 audit_selected: bool,
202 ) -> Result<()> {
203 self.conn.lock().unwrap().execute(
204 "INSERT INTO psp9_calibration_samples \
205 (epoch_id, sample_id, score, unsafe_label, audit_selected) VALUES (?, ?, ?, ?, ?) \
206 ON CONFLICT (epoch_id, sample_id) DO NOTHING",
207 duckdb::params![epoch_id, sample_id, score, unsafe_label, audit_selected],
208 )?;
209 Ok(())
210 }
211
212 pub fn label_psp9_calibration_sample(&self, sample_id: &str, is_unsafe: bool) -> Result<usize> {
216 let updated = self.conn.lock().unwrap().execute(
217 "UPDATE psp9_calibration_samples SET unsafe_label = ? \
218 WHERE sample_id = ? AND unsafe_label IS NULL",
219 duckdb::params![is_unsafe, sample_id],
220 )?;
221 Ok(updated)
222 }
223
224 pub fn label_psp9_verdicts(&self, candidate_id: &str, is_unsafe: bool) -> Result<usize> {
228 let updated = self.conn.lock().unwrap().execute(
229 "UPDATE psp9_verdicts SET unsafe_label = ? \
230 WHERE candidate_id = ? AND unsafe_label IS NULL",
231 duckdb::params![is_unsafe, candidate_id],
232 )?;
233 Ok(updated)
234 }
235
236 pub fn labeled_psp9_verdicts(&self) -> Result<Vec<Psp9VerdictRow>> {
239 let conn = self.conn.lock().unwrap();
240 let mut statement = conn.prepare(
241 "SELECT session_id, candidate_id, validator_id, stratum, missed, \
242 unsafe_label, evidence_hash FROM psp9_verdicts \
243 WHERE unsafe_label IS NOT NULL ORDER BY candidate_id, validator_id",
244 )?;
245 let rows = statement.query_map([], |row| {
246 Ok(Psp9VerdictRow {
247 session_id: row.get(0)?,
248 candidate_id: row.get(1)?,
249 validator_id: row.get(2)?,
250 stratum: row.get(3)?,
251 missed: row.get(4)?,
252 unsafe_label: row.get(5)?,
253 evidence_hash: row.get(6)?,
254 })
255 })?;
256 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
257 }
258
259 pub fn pending_psp9_audit_samples(&self, limit: usize) -> Result<Vec<(String, String, f64)>> {
261 let conn = self.conn.lock().unwrap();
262 let mut statement = conn.prepare(
263 "SELECT epoch_id, sample_id, score FROM psp9_calibration_samples \
264 WHERE audit_selected AND unsafe_label IS NULL \
265 ORDER BY created_at LIMIT ?",
266 )?;
267 let rows =
268 statement.query_map([limit], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
269 rows.collect::<std::result::Result<Vec<_>, _>>()
270 .map_err(Into::into)
271 }
272
273 pub fn labeled_psp9_samples_for_stratum(&self, stratum: &str) -> Result<Vec<(f64, bool)>> {
277 let conn = self.conn.lock().unwrap();
278 let mut statement = conn.prepare(
279 "SELECT s.score, s.unsafe_label \
280 FROM psp9_calibration_samples s \
281 JOIN psp9_calibration_epochs e ON e.epoch_id = s.epoch_id \
282 WHERE e.stratum = ? AND s.unsafe_label IS NOT NULL \
283 ORDER BY s.created_at, s.sample_id",
284 )?;
285 let rows = statement.query_map([stratum], |row| Ok((row.get(0)?, row.get(1)?)))?;
286 rows.collect::<std::result::Result<Vec<_>, _>>()
287 .map_err(Into::into)
288 }
289
290 pub fn all_psp9_calibration_epochs(
292 &self,
293 limit: usize,
294 ) -> Result<Vec<Psp9CalibrationEpochRow>> {
295 let conn = self.conn.lock().unwrap();
296 let mut statement = conn.prepare(
297 "SELECT epoch_id, stratum, target_rho, threshold, state, sample_count \
298 FROM psp9_calibration_epochs ORDER BY created_at DESC, epoch_id DESC LIMIT ?",
299 )?;
300 let rows = statement.query_map([limit], |row| {
301 Ok(Psp9CalibrationEpochRow {
302 epoch_id: row.get(0)?,
303 stratum: row.get(1)?,
304 target_rho: row.get(2)?,
305 threshold: row.get(3)?,
306 state: row.get(4)?,
307 sample_count: row.get(5)?,
308 })
309 })?;
310 rows.collect::<std::result::Result<Vec<_>, _>>()
311 .map_err(Into::into)
312 }
313
314 pub fn psp9_epoch_stratum(&self, epoch_id: &str) -> Result<Option<String>> {
316 let conn = self.conn.lock().unwrap();
317 let mut statement =
318 conn.prepare("SELECT stratum FROM psp9_calibration_epochs WHERE epoch_id = ?")?;
319 let mut rows = statement.query([epoch_id])?;
320 Ok(rows.next()?.map(|row| row.get(0)).transpose()?)
321 }
322
323 pub fn record_psp9_event(&self, row: &Psp9LedgerRow) -> Result<()> {
325 self.conn.lock().unwrap().execute(
326 "INSERT INTO psp9_ledger_events (session_id, sequence, event_json, prev_hash, hash) \
327 VALUES (?, ?, ?, ?, ?)",
328 duckdb::params![
329 row.session_id,
330 row.sequence,
331 row.event_json,
332 row.prev_hash,
333 row.hash
334 ],
335 )?;
336 Ok(())
337 }
338
339 pub fn get_psp9_events(&self, session_id: &str) -> Result<Vec<Psp9LedgerRow>> {
341 let conn = self.conn.lock().unwrap();
342 let mut stmt = conn.prepare(
343 "SELECT session_id, sequence, event_json, prev_hash, hash \
344 FROM psp9_ledger_events WHERE session_id = ? ORDER BY sequence",
345 )?;
346 let rows = stmt.query_map([session_id], |row| {
347 Ok(Psp9LedgerRow {
348 session_id: row.get(0)?,
349 sequence: row.get(1)?,
350 event_json: row.get(2)?,
351 prev_hash: row.get(3)?,
352 hash: row.get(4)?,
353 })
354 })?;
355 let mut results = Vec::new();
356 for row in rows {
357 results.push(row?);
358 }
359 Ok(results)
360 }
361
362 pub fn initialize_authority_epoch(&self, session_id: &str, epoch: u64) -> Result<()> {
363 self.conn.lock().unwrap().execute(
364 "INSERT INTO psp9_authority_epochs (session_id, epoch) VALUES (?, ?) \
365 ON CONFLICT (session_id) DO NOTHING",
366 duckdb::params![session_id, epoch as i64],
367 )?;
368 Ok(())
369 }
370
371 pub fn authority_epoch(&self, session_id: &str) -> Result<u64> {
372 let conn = self.conn.lock().unwrap();
373 let mut statement =
374 conn.prepare("SELECT epoch FROM psp9_authority_epochs WHERE session_id = ?")?;
375 let mut rows = statement.query([session_id])?;
376 let row = rows.next()?.context("authority epoch is not initialized")?;
377 let epoch: i64 = row.get(0)?;
378 Ok(epoch as u64)
379 }
380
381 pub fn with_authority_epoch<T>(
385 &self,
386 session_id: &str,
387 expected_epoch: u64,
388 operation: impl FnOnce() -> Result<T>,
389 ) -> Result<T> {
390 let mut conn = self.conn.lock().unwrap();
391 let transaction = conn.transaction()?;
392 let updated = transaction.execute(
393 "UPDATE psp9_authority_epochs SET epoch = epoch WHERE session_id = ?",
394 [session_id],
395 )?;
396 anyhow::ensure!(updated == 1, "authority epoch is not initialized");
397 let epoch: i64 = transaction.query_row(
398 "SELECT epoch FROM psp9_authority_epochs WHERE session_id = ?",
399 [session_id],
400 |row| row.get(0),
401 )?;
402 anyhow::ensure!(
403 epoch as u64 == expected_epoch,
404 "authority epoch changed before durable effect: expected {expected_epoch}, found {epoch}"
405 );
406 let output = operation()?;
407 transaction.commit()?;
408 Ok(output)
409 }
410
411 pub fn revoke_authority(&self, session_id: &str) -> Result<u64> {
412 let mut conn = self.conn.lock().unwrap();
413 let transaction = conn.transaction()?;
414 transaction.execute(
415 "UPDATE psp9_authority_epochs SET epoch = epoch + 1 WHERE session_id = ?",
416 [session_id],
417 )?;
418 let epoch: i64 = transaction.query_row(
419 "SELECT epoch FROM psp9_authority_epochs WHERE session_id = ?",
420 [session_id],
421 |row| row.get(0),
422 )?;
423 transaction.commit()?;
424 Ok(epoch as u64)
425 }
426
427 pub fn record_grant_policy(
428 &self,
429 session_id: &str,
430 policy_id: &str,
431 policy_json: &str,
432 ) -> Result<()> {
433 self.conn.lock().unwrap().execute(
434 "INSERT INTO psp9_grant_policies (policy_id, session_id, policy_json) \
435 VALUES (?, ?, ?)",
436 duckdb::params![policy_id, session_id, policy_json],
437 )?;
438 Ok(())
439 }
440
441 pub fn get_grant_policy(&self, session_id: &str) -> Result<Option<String>> {
443 let conn = self.conn.lock().unwrap();
444 let mut statement = conn.prepare(
445 "SELECT policy_json FROM psp9_grant_policies WHERE session_id = ? \
446 ORDER BY created_at DESC, policy_id DESC LIMIT 1",
447 )?;
448 let mut rows = statement.query([session_id])?;
449 Ok(rows.next()?.map(|row| row.get(0)).transpose()?)
450 }
451
452 pub fn record_external_effect_intent(
456 &self,
457 session_id: &str,
458 idempotency_key: &str,
459 intent_hash: &str,
460 intent_json: &str,
461 ) -> Result<()> {
462 let mut conn = self.conn.lock().unwrap();
463 let transaction = conn.transaction()?;
464 let existing: Option<String> = transaction
465 .query_row(
466 "SELECT intent_hash FROM psp9_external_effects \
467 WHERE session_id = ? AND idempotency_key = ?",
468 duckdb::params![session_id, idempotency_key],
469 |row| row.get(0),
470 )
471 .ok();
472 match existing {
473 Some(hash) if hash == intent_hash => {}
474 Some(_) => anyhow::bail!("idempotency key reused for different external effect"),
475 None => {
476 transaction.execute(
477 "INSERT INTO psp9_external_effects \
478 (session_id, idempotency_key, intent_hash, intent_json, status) \
479 VALUES (?, ?, ?, ?, 'INTENT_RECORDED')",
480 duckdb::params![session_id, idempotency_key, intent_hash, intent_json],
481 )?;
482 }
483 }
484 transaction.commit()?;
485 Ok(())
486 }
487
488 pub fn complete_external_effect(
489 &self,
490 session_id: &str,
491 idempotency_key: &str,
492 result_json: &str,
493 ) -> Result<()> {
494 let updated = self.conn.lock().unwrap().execute(
495 "UPDATE psp9_external_effects SET result_json = ?, status = 'COMPLETED', \
496 completed_at = CURRENT_TIMESTAMP \
497 WHERE session_id = ? AND idempotency_key = ? AND status = 'INTENT_RECORDED'",
498 duckdb::params![result_json, session_id, idempotency_key],
499 )?;
500 anyhow::ensure!(
501 updated == 1,
502 "external effect has no pending write-ahead intent"
503 );
504 Ok(())
505 }
506
507 pub fn completed_external_effects(
509 &self,
510 session_id: &str,
511 ) -> Result<Vec<Psp9ExternalEffectRow>> {
512 let conn = self.conn.lock().unwrap();
513 let mut statement = conn.prepare(
514 "SELECT idempotency_key, intent_hash, intent_json, result_json, status \
515 FROM psp9_external_effects WHERE session_id = ? AND status = 'COMPLETED' \
516 ORDER BY created_at DESC",
517 )?;
518 let rows = statement.query_map([session_id], |row| {
519 Ok(Psp9ExternalEffectRow {
520 idempotency_key: row.get(0)?,
521 intent_hash: row.get(1)?,
522 intent_json: row.get(2)?,
523 result_json: row.get(3)?,
524 status: row.get(4)?,
525 })
526 })?;
527 rows.collect::<std::result::Result<Vec<_>, _>>()
528 .map_err(Into::into)
529 }
530
531 pub fn pending_external_effects(&self, session_id: &str) -> Result<Vec<Psp9ExternalEffectRow>> {
532 let conn = self.conn.lock().unwrap();
533 let mut statement = conn.prepare(
534 "SELECT idempotency_key, intent_hash, intent_json, result_json, status \
535 FROM psp9_external_effects WHERE session_id = ? AND status != 'COMPLETED' \
536 ORDER BY created_at",
537 )?;
538 let rows = statement.query_map([session_id], |row| {
539 Ok(Psp9ExternalEffectRow {
540 idempotency_key: row.get(0)?,
541 intent_hash: row.get(1)?,
542 intent_json: row.get(2)?,
543 result_json: row.get(3)?,
544 status: row.get(4)?,
545 })
546 })?;
547 rows.collect::<std::result::Result<Vec<_>, _>>()
548 .map_err(Into::into)
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 fn scratch_store() -> (std::path::PathBuf, SessionStore) {
557 let dir = std::env::temp_dir().join(format!("perspt_p9_{}", uuid::Uuid::new_v4()));
558 std::fs::create_dir_all(&dir).unwrap();
559 let store = SessionStore::open(&dir.join("t.db")).unwrap();
560 (dir, store)
561 }
562
563 #[test]
564 fn psp9_events_round_trip_in_order() {
565 let (dir, store) = scratch_store();
566 for sequence in 0..3i64 {
567 store
568 .record_psp9_event(&Psp9LedgerRow {
569 session_id: "s1".into(),
570 sequence,
571 event_json: format!("{{\"seq\":{sequence}}}"),
572 prev_hash: format!("h{}", sequence.saturating_sub(1)),
573 hash: format!("h{sequence}"),
574 })
575 .unwrap();
576 }
577 let rows = store.get_psp9_events("s1").unwrap();
578 assert_eq!(rows.len(), 3);
579 assert_eq!(rows[2].hash, "h2");
580 assert!(store.get_psp9_events("other").unwrap().is_empty());
581 let _ = std::fs::remove_dir_all(&dir);
582 }
583
584 #[test]
585 fn external_effects_are_single_assignment() {
586 let (dir, store) = scratch_store();
587 store
588 .record_external_effect_intent("s1", "promote:n1", "h1", "{\"a\":1}")
589 .unwrap();
590 store
591 .record_external_effect_intent("s1", "promote:n1", "h1", "{\"a\":1}")
592 .unwrap();
593 assert!(store
594 .record_external_effect_intent("s1", "promote:n1", "h2", "{\"a\":2}")
595 .is_err());
596 store
597 .complete_external_effect("s1", "promote:n1", "{\"ok\":true}")
598 .unwrap();
599 assert!(store.pending_external_effects("s1").unwrap().is_empty());
600 let _ = std::fs::remove_dir_all(&dir);
601 }
602
603 #[test]
604 fn artifacts_verdicts_and_calibration_rows_persist() {
605 let (dir, store) = scratch_store();
606 let bytes = b"complete compiler output";
607 let hash = "artifact-hash".to_string();
608 store.put_psp9_artifact(&hash, bytes, "text/plain").unwrap();
609 assert_eq!(store.get_psp9_artifact(&hash).unwrap().unwrap(), bytes);
610 store
611 .record_psp9_checkpoint("s1", "root-1", "{\"control\":{}}")
612 .unwrap();
613 store
614 .record_psp9_verdict(&Psp9VerdictRow {
615 session_id: "s1".into(),
616 candidate_id: "c1".into(),
617 validator_id: "cargo-test".into(),
618 stratum: "rust:test:v1".into(),
619 missed: false,
620 unsafe_label: Some(false),
621 evidence_hash: hash.clone(),
622 })
623 .unwrap();
624 store
625 .record_psp9_calibration_epoch(&Psp9CalibrationEpochRow {
626 epoch_id: "e1".into(),
627 stratum: "rust:test:v1".into(),
628 target_rho: 0.1,
629 threshold: Some(0.5),
630 state: "shadow".into(),
631 sample_count: 1,
632 })
633 .unwrap();
634 store
635 .record_psp9_calibration_sample("e1", "sample-1", 0.2, None, true)
636 .unwrap();
637 assert_eq!(
638 store
639 .latest_psp9_calibration_epoch("rust:test:v1")
640 .unwrap()
641 .unwrap()
642 .state,
643 "shadow"
644 );
645 assert_eq!(
646 store.psp9_calibration_samples("e1").unwrap(),
647 vec![("sample-1".into(), 0.2, None, true)]
648 );
649 let _ = std::fs::remove_dir_all(&dir);
650 }
651
652 #[test]
653 fn delayed_audit_labels_are_single_assignment() {
654 let (dir, store) = scratch_store();
655 store
656 .record_psp9_calibration_epoch(&Psp9CalibrationEpochRow {
657 epoch_id: "e1".into(),
658 stratum: "rust:test:v1".into(),
659 target_rho: 0.1,
660 threshold: None,
661 state: "insufficient_samples".into(),
662 sample_count: 0,
663 })
664 .unwrap();
665 store
666 .record_psp9_calibration_sample("e1", "sample-1", 0.2, None, true)
667 .unwrap();
668 assert_eq!(
669 store.pending_psp9_audit_samples(10).unwrap(),
670 vec![("e1".into(), "sample-1".into(), 0.2)]
671 );
672 assert_eq!(
673 store
674 .label_psp9_calibration_sample("sample-1", false)
675 .unwrap(),
676 1
677 );
678 assert_eq!(
680 store
681 .label_psp9_calibration_sample("sample-1", true)
682 .unwrap(),
683 0
684 );
685 assert_eq!(
686 store
687 .labeled_psp9_samples_for_stratum("rust:test:v1")
688 .unwrap(),
689 vec![(0.2, false)]
690 );
691 assert_eq!(
692 store.psp9_epoch_stratum("e1").unwrap().as_deref(),
693 Some("rust:test:v1")
694 );
695 let _ = std::fs::remove_dir_all(&dir);
696 }
697
698 #[test]
699 fn durable_effect_is_guarded_by_the_exact_authority_epoch() {
700 let (_dir, store) = scratch_store();
701 store.initialize_authority_epoch("s1", 3).unwrap();
702 let ran = std::sync::atomic::AtomicBool::new(false);
703 store
704 .with_authority_epoch("s1", 3, || {
705 ran.store(true, std::sync::atomic::Ordering::SeqCst);
706 Ok(())
707 })
708 .unwrap();
709 assert!(ran.load(std::sync::atomic::Ordering::SeqCst));
710
711 store.revoke_authority("s1").unwrap();
712 ran.store(false, std::sync::atomic::Ordering::SeqCst);
713 assert!(store
714 .with_authority_epoch("s1", 3, || {
715 ran.store(true, std::sync::atomic::Ordering::SeqCst);
716 Ok(())
717 })
718 .is_err());
719 assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
720 }
721}