tawasuyu

tawasuyu/brahman

Rust 230TOML 69JSON 14HTML 12901 Definiciones

⎇ f0c0a718605924227076bef7cb8b403c388261b6 crates/modules/nakui/core/src/store.rs Rust · 695 líneas · 25.3 KB Historia Quién tocó qué crudo

feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío f0c0a7186 · Sergio ·

1use serde_json::Value;
2use sha2::{Digest, Sha256};
3use std::collections::HashMap;
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::delta::FieldOp;
8
9#[derive(Debug, Clone, Error)]
10pub enum StoreError {
11    #[error("entity {0} id {1} not found")]
12    NotFound(String, Uuid),
13    #[error("entity {0} id {1} already exists")]
14    Conflict(String, Uuid),
15    #[error("set on non-object record at {0} {1}")]
16    NotAnObject(String, Uuid),
17    /// Backend-specific transient or systemic failure (network, disk,
18    /// driver). Distinct from the data-shape errors above.
19    #[error("backend error: {0}")]
20    Backend(String),
21}
22
23pub trait Store {
24    fn load(&self, entity: &str, id: Uuid) -> Option<Value>;
25
26    /// Insert or replace a record without going through the morphism
27    /// pipeline. Represents external/boundary input — the source of
28    /// records that didn't originate from a kernel-validated event.
29    fn seed(&mut self, entity: &str, id: Uuid, data: Value);
30
31    /// Read-only check: would `apply(ops)` succeed against current state?
32    /// Does NOT mutate. The kernel runs this as the last step of `compute`
33    /// so that, by the time we log an event, the apply is contractually
34    /// guaranteed to land.
35    fn apply_dry_run(&self, ops: &[FieldOp]) -> Result<(), StoreError>;
36
37    fn apply(&mut self, ops: &[FieldOp]) -> Result<(), StoreError>;
38
39    /// Drop every record. Used by `reconcile` to wipe a stale store before
40    /// replaying the log. Must leave the store in the same state it would
41    /// be in immediately after construction. Implementors that override
42    /// `last_applied_seq` must reset that marker here too — a cleared
43    /// store has applied nothing.
44    fn clear(&mut self) -> Result<(), StoreError>;
45
46    /// The last log seq whose effects are reflected in this store, if
47    /// the store can persist that fact. Default `Ok(None)` covers
48    /// transient backends. The startup path uses this to skip the full
49    /// replay when the store is verifiably already in sync with the log.
50    fn last_applied_seq(&self) -> Result<Option<u64>, StoreError> {
51        Ok(None)
52    }
53
54    /// Persist the marker after a successful apply / seed / replay.
55    /// Best-effort: callers ignore failures here because a stale marker
56    /// only costs an extra full replay on next startup, never
57    /// correctness — full replay starts with `clear()`, so it tolerates
58    /// any prior state. Default impl is a no-op for transient backends.
59    fn set_last_applied_seq(&mut self, _seq: u64) -> Result<(), StoreError> {
60        Ok(())
61    }
62
63    /// Enumerate every record in canonical order: sorted first by entity
64    /// name, then by id bytes. The canonical order is what makes
65    /// `hash_state` reproducible — without it two stores with the same
66    /// records would hash differently depending on insertion order.
67    ///
68    /// Returns owned `Value`s. For an in-memory backend this clones; for
69    /// a remote backend it materializes a snapshot. V1 chooses simplicity
70    /// over streaming — the hash and drift-comparison use cases need to
71    /// see all records anyway, and an iterator over a Vec keeps the
72    /// trait method object-safe and free of async lifetime concerns.
73    fn iter(&self) -> Result<Box<dyn Iterator<Item = (String, Uuid, Value)> + '_>, StoreError>;
74
75    /// Deterministic SHA-256 of the store's full state. Two stores with
76    /// the same records (regardless of how they got there or which
77    /// backend they live in) produce the same hash; any drift produces
78    /// a different one. The default impl is the contract — backends
79    /// should only override it for backend-native acceleration (e.g.
80    /// server-side table digests), and an override must produce the
81    /// same bytes as the default.
82    ///
83    /// Framing per record:
84    ///   entity_bytes | 0x00 | id_bytes | 0x00 | canonical_value_hash
85    /// The length prefix on entity/id prevents (entity="ab", id="c")
86    /// from colliding with (entity="a", id="bc"). The value bytes are
87    /// produced by `hash_value`, which walks the JSON tree with
88    /// type-tagged framing — that decouples the hash from
89    /// `serde_json::to_vec`'s representation choices (especially
90    /// integer-valued floats vs ints) so cross-backend comparison
91    /// works.
92    fn hash_state(&self) -> Result<[u8; 32], StoreError> {
93        let mut hasher = Sha256::new();
94        for (entity, id, value) in self.iter()? {
95            hasher.update(entity.as_bytes());
96            hasher.update([0u8]);
97            hasher.update(id.as_bytes());
98            hasher.update([0u8]);
99            hash_value(&mut hasher, &value);
100        }
101        Ok(hasher.finalize().into())
102    }
103}
104
105/// Canonical hash of a `serde_json::Value`. Type-tagged so a string
106/// "true" can't collide with the boolean `true`; length-prefixed so
107/// concatenation can't shift bytes between fields. Numbers normalize:
108/// any integer-valued number (i64, u64, or a finite f64 with no
109/// fractional part) is hashed as an i128 — that's what makes
110/// cross-backend equality work, since SurrealDB may round-trip
111/// what the caller wrote as `100_i64` back as the same numeric value
112/// without us needing to commit to a wire-format-specific
113/// representation.
114pub fn hash_value(hasher: &mut Sha256, v: &Value) {
115    match v {
116        Value::Null => hasher.update([TAG_NULL]),
117        Value::Bool(b) => {
118            hasher.update([TAG_BOOL]);
119            hasher.update([*b as u8]);
120        }
121        Value::Number(n) => {
122            if let Some(i) = n.as_i64() {
123                hash_int(hasher, i as i128);
124            } else if let Some(u) = n.as_u64() {
125                hash_int(hasher, u as i128);
126            } else if let Some(f) = n.as_f64() {
127                // Integer-valued floats canonicalize to int. Anything
128                // else (fractions, NaN, infinities) hashes as the raw
129                // f64 bit pattern — that's still deterministic, just
130                // not normalized.
131                if f.is_finite()
132                    && f.fract() == 0.0
133                    && f >= I128_MIN_AS_F64
134                    && f <= I128_MAX_AS_F64
135                {
136                    hash_int(hasher, f as i128);
137                } else {
138                    hasher.update([TAG_FLOAT]);
139                    hasher.update(f.to_bits().to_le_bytes());
140                }
141            } else {
142                // serde_json::Number guarantees one of the above; this
143                // branch only fires if a future variant appears.
144                hasher.update([TAG_FLOAT]);
145                hasher.update(f64::NAN.to_bits().to_le_bytes());
146            }
147        }
148        Value::String(s) => {
149            hasher.update([TAG_STRING]);
150            hasher.update((s.len() as u64).to_le_bytes());
151            hasher.update(s.as_bytes());
152        }
153        Value::Array(arr) => {
154            hasher.update([TAG_ARRAY]);
155            hasher.update((arr.len() as u64).to_le_bytes());
156            for item in arr {
157                hash_value(hasher, item);
158            }
159        }
160        Value::Object(map) => {
161            hasher.update([TAG_OBJECT]);
162            hasher.update((map.len() as u64).to_le_bytes());
163            // serde_json::Map without `preserve_order` is BTreeMap
164            // (alphabetical). We sort defensively in case the build
165            // pulls in `preserve_order` transitively from a future dep.
166            let mut keys: Vec<&String> = map.keys().collect();
167            keys.sort();
168            for k in keys {
169                hasher.update((k.len() as u64).to_le_bytes());
170                hasher.update(k.as_bytes());
171                hash_value(hasher, &map[k]);
172            }
173        }
174    }
175}
176
177fn hash_int(hasher: &mut Sha256, n: i128) {
178    hasher.update([TAG_INT]);
179    hasher.update(n.to_le_bytes());
180}
181
182const TAG_NULL: u8 = 0;
183const TAG_BOOL: u8 = 1;
184const TAG_INT: u8 = 2;
185const TAG_FLOAT: u8 = 3;
186const TAG_STRING: u8 = 4;
187const TAG_ARRAY: u8 = 5;
188const TAG_OBJECT: u8 = 6;
189
190// f64 can't represent i128::MAX exactly; the cast truncates upward to
191// the next representable f64. Use those as the comparison bounds so
192// `f as i128` stays well-defined.
193const I128_MIN_AS_F64: f64 = -1.7014118346046923e38;
194const I128_MAX_AS_F64: f64 = 1.7014118346046923e38;
195
196#[derive(Debug, Default, Clone, PartialEq)]
197pub struct MemoryStore {
198    records: HashMap<String, HashMap<Uuid, Value>>,
199    /// Last log seq whose effects are reflected here. In-process only —
200    /// resets to `None` on construction or `clear`. The skip-replay
201    /// optimization in `nakui run` benefits the persistent backends;
202    /// for `MemoryStore` it's harmless bookkeeping (process restart =
203    /// new store = `None`, which forces full replay).
204    last_applied: Option<u64>,
205}
206
207impl MemoryStore {
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Borrow the internal records map. Used by `Snapshot::from_memory_store`
213    /// to capture state for snapshot persistence.
214    pub fn records(&self) -> &HashMap<String, HashMap<Uuid, Value>> {
215        &self.records
216    }
217}
218
219impl Store for MemoryStore {
220    fn load(&self, entity: &str, id: Uuid) -> Option<Value> {
221        self.records.get(entity)?.get(&id).cloned()
222    }
223
224    fn seed(&mut self, entity: &str, id: Uuid, data: Value) {
225        self.records
226            .entry(entity.to_string())
227            .or_default()
228            .insert(id, data);
229    }
230
231    fn apply_dry_run(&self, ops: &[FieldOp]) -> Result<(), StoreError> {
232        for op in ops {
233            match op {
234                FieldOp::Set { path, .. } | FieldOp::Clear { path } => {
235                    // Set y Clear comparten la misma pre-condición: el
236                    // record padre tiene que existir y ser un objeto.
237                    // Clear de un field que no existe en el map es no-op
238                    // benigno en apply (no error).
239                    match self.records.get(&path.entity).and_then(|m| m.get(&path.id)) {
240                        None => {
241                            return Err(StoreError::NotFound(path.entity.clone(), path.id));
242                        }
243                        Some(Value::Object(_)) => {}
244                        Some(_) => {
245                            return Err(StoreError::NotAnObject(path.entity.clone(), path.id));
246                        }
247                    }
248                }
249                FieldOp::Create { entity, id, .. } => {
250                    if self
251                        .records
252                        .get(entity)
253                        .and_then(|m| m.get(id))
254                        .is_some()
255                    {
256                        return Err(StoreError::Conflict(entity.clone(), *id));
257                    }
258                }
259                FieldOp::Delete { entity, id } => {
260                    if self
261                        .records
262                        .get(entity)
263                        .and_then(|m| m.get(id))
264                        .is_none()
265                    {
266                        return Err(StoreError::NotFound(entity.clone(), *id));
267                    }
268                }
269            }
270        }
271        Ok(())
272    }
273
274    fn apply(&mut self, ops: &[FieldOp]) -> Result<(), StoreError> {
275        self.apply_dry_run(ops)?;
276        for op in ops {
277            match op {
278                FieldOp::Set { path, value } => {
279                    let rec = self
280                        .records
281                        .get_mut(&path.entity)
282                        .and_then(|m| m.get_mut(&path.id))
283                        .expect("validated by dry_run");
284                    let map = match rec {
285                        Value::Object(m) => m,
286                        _ => unreachable!("dry_run guards against non-object"),
287                    };
288                    map.insert(path.field.clone(), value.clone());
289                }
290                FieldOp::Clear { path } => {
291                    let rec = self
292                        .records
293                        .get_mut(&path.entity)
294                        .and_then(|m| m.get_mut(&path.id))
295                        .expect("validated by dry_run");
296                    let map = match rec {
297                        Value::Object(m) => m,
298                        _ => unreachable!("dry_run guards against non-object"),
299                    };
300                    // Clear de un field ausente: no-op silencioso. El
301                    // post-state es el mismo (el field no está) y permite
302                    // que el caller emita Clear sin hacer load previo.
303                    map.remove(&path.field);
304                }
305                FieldOp::Create { entity, id, data } => {
306                    self.records
307                        .entry(entity.clone())
308                        .or_default()
309                        .insert(*id, data.clone());
310                }
311                FieldOp::Delete { entity, id } => {
312                    self.records
313                        .get_mut(entity)
314                        .expect("validated by dry_run")
315                        .remove(id);
316                }
317            }
318        }
319        Ok(())
320    }
321
322    fn clear(&mut self) -> Result<(), StoreError> {
323        self.records.clear();
324        self.last_applied = None;
325        Ok(())
326    }
327
328    fn last_applied_seq(&self) -> Result<Option<u64>, StoreError> {
329        Ok(self.last_applied)
330    }
331
332    fn set_last_applied_seq(&mut self, seq: u64) -> Result<(), StoreError> {
333        self.last_applied = Some(seq);
334        Ok(())
335    }
336
337    fn iter(&self) -> Result<Box<dyn Iterator<Item = (String, Uuid, Value)> + '_>, StoreError> {
338        let mut out: Vec<(String, Uuid, Value)> = self
339            .records
340            .iter()
341            .flat_map(|(entity, m)| {
342                m.iter()
343                    .map(move |(id, v)| (entity.clone(), *id, v.clone()))
344            })
345            .collect();
346        out.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.as_bytes().cmp(b.1.as_bytes())));
347        Ok(Box::new(out.into_iter()))
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use crate::delta::{FieldOp, FieldPath};
355    use serde_json::json;
356
357    #[test]
358    fn dry_run_rejects_set_on_non_object() {
359        let mut store = MemoryStore::new();
360        let id = Uuid::new_v4();
361        store.seed("Caja", id, json!(42)); // not an object
362        let op = FieldOp::Set {
363            path: FieldPath {
364                entity: "Caja".into(),
365                id,
366                field: "saldo".into(),
367            },
368            value: json!(100),
369        };
370        match store.apply_dry_run(&[op.clone()]) {
371            Err(StoreError::NotAnObject(e, i)) => {
372                assert_eq!(e, "Caja");
373                assert_eq!(i, id);
374            }
375            other => panic!("expected NotAnObject, got {:?}", other),
376        }
377        // apply must reject too without panicking.
378        assert!(matches!(
379            store.apply(&[op]),
380            Err(StoreError::NotAnObject(_, _))
381        ));
382    }
383
384    #[test]
385    fn dry_run_rejects_create_conflict() {
386        let mut store = MemoryStore::new();
387        let id = Uuid::new_v4();
388        store.seed("Caja", id, json!({"id": id.to_string()}));
389        let op = FieldOp::Create {
390            entity: "Caja".into(),
391            id,
392            data: json!({"id": id.to_string()}),
393        };
394        assert!(matches!(
395            store.apply_dry_run(&[op]),
396            Err(StoreError::Conflict(_, _))
397        ));
398    }
399
400    #[test]
401    fn dry_run_passes_for_valid_set() {
402        let mut store = MemoryStore::new();
403        let id = Uuid::new_v4();
404        store.seed("Caja", id, json!({"saldo": 100, "currency": "USD"}));
405        let op = FieldOp::Set {
406            path: FieldPath {
407                entity: "Caja".into(),
408                id,
409                field: "saldo".into(),
410            },
411            value: json!(150),
412        };
413        assert!(store.apply_dry_run(&[op]).is_ok());
414    }
415
416    #[test]
417    fn apply_clear_removes_field_key() {
418        let mut store = MemoryStore::new();
419        let id = Uuid::new_v4();
420        store.seed(
421            "Customer",
422            id,
423            json!({"id": id.to_string(), "name": "Acme", "notes": "lorem"}),
424        );
425        let op = FieldOp::Clear {
426            path: FieldPath {
427                entity: "Customer".into(),
428                id,
429                field: "notes".into(),
430            },
431        };
432        store.apply(&[op]).unwrap();
433        let after = store.load("Customer", id).unwrap();
434        let map = after.as_object().unwrap();
435        assert!(!map.contains_key("notes"), "notes debería estar borrado");
436        assert_eq!(map.get("name"), Some(&json!("Acme")), "otros fields intactos");
437    }
438
439    #[test]
440    fn apply_clear_on_absent_field_is_noop() {
441        let mut store = MemoryStore::new();
442        let id = Uuid::new_v4();
443        store.seed(
444            "Customer",
445            id,
446            json!({"id": id.to_string(), "name": "Acme"}),
447        );
448        let op = FieldOp::Clear {
449            path: FieldPath {
450                entity: "Customer".into(),
451                id,
452                field: "notes".into(),
453            },
454        };
455        // No debería errar: clear de un field ausente es benigno.
456        store.apply(&[op]).unwrap();
457        let after = store.load("Customer", id).unwrap();
458        assert_eq!(
459            after.as_object().unwrap().get("name"),
460            Some(&json!("Acme"))
461        );
462    }
463
464    #[test]
465    fn dry_run_rejects_clear_on_missing_record() {
466        let store = MemoryStore::new();
467        let id = Uuid::new_v4();
468        let op = FieldOp::Clear {
469            path: FieldPath {
470                entity: "Customer".into(),
471                id,
472                field: "notes".into(),
473            },
474        };
475        assert!(matches!(
476            store.apply_dry_run(&[op]),
477            Err(StoreError::NotFound(_, _))
478        ));
479    }
480
481    #[test]
482    fn dry_run_rejects_clear_on_non_object() {
483        let mut store = MemoryStore::new();
484        let id = Uuid::new_v4();
485        store.seed("Customer", id, json!(42)); // not an object
486        let op = FieldOp::Clear {
487            path: FieldPath {
488                entity: "Customer".into(),
489                id,
490                field: "notes".into(),
491            },
492        };
493        assert!(matches!(
494            store.apply_dry_run(&[op]),
495            Err(StoreError::NotAnObject(_, _))
496        ));
497    }
498
499    #[test]
500    fn iter_returns_canonical_order_regardless_of_insertion() {
501        let a = Uuid::new_v4();
502        let b = Uuid::new_v4();
503        let c = Uuid::new_v4();
504
505        let mut s1 = MemoryStore::new();
506        s1.seed("Caja", a, json!({"id": a.to_string(), "x": 1}));
507        s1.seed("Movimiento", c, json!({"id": c.to_string(), "y": 3}));
508        s1.seed("Caja", b, json!({"id": b.to_string(), "x": 2}));
509
510        let mut s2 = MemoryStore::new();
511        s2.seed("Movimiento", c, json!({"id": c.to_string(), "y": 3}));
512        s2.seed("Caja", b, json!({"id": b.to_string(), "x": 2}));
513        s2.seed("Caja", a, json!({"id": a.to_string(), "x": 1}));
514
515        let r1: Vec<_> = s1.iter().unwrap().collect();
516        let r2: Vec<_> = s2.iter().unwrap().collect();
517        assert_eq!(r1, r2, "iter order must be insertion-independent");
518
519        // Entities lexicographically sorted (Caja before Movimiento).
520        let entities: Vec<&str> = r1.iter().map(|(e, _, _)| e.as_str()).collect();
521        assert_eq!(entities, vec!["Caja", "Caja", "Movimiento"]);
522
523        // Within Caja, ids in byte order.
524        let caja_ids: Vec<Uuid> = r1
525            .iter()
526            .filter(|(e, _, _)| e == "Caja")
527            .map(|(_, id, _)| *id)
528            .collect();
529        let mut expected = vec![a, b];
530        expected.sort_by(|x, y| x.as_bytes().cmp(y.as_bytes()));
531        assert_eq!(caja_ids, expected);
532    }
533
534    #[test]
535    fn hash_state_is_deterministic_and_independent_of_insertion_order() {
536        let a = Uuid::new_v4();
537        let b = Uuid::new_v4();
538
539        let mut s1 = MemoryStore::new();
540        s1.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
541        s1.seed("Caja", b, json!({"id": b.to_string(), "saldo": 200}));
542
543        let mut s2 = MemoryStore::new();
544        s2.seed("Caja", b, json!({"id": b.to_string(), "saldo": 200}));
545        s2.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
546
547        assert_eq!(
548            s1.hash_state().unwrap(),
549            s2.hash_state().unwrap(),
550            "equal state must hash identically regardless of how it was built"
551        );
552    }
553
554    #[test]
555    fn hash_state_changes_when_state_changes() {
556        let a = Uuid::new_v4();
557
558        let mut s1 = MemoryStore::new();
559        s1.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
560
561        let mut s2 = MemoryStore::new();
562        s2.seed("Caja", a, json!({"id": a.to_string(), "saldo": 101}));
563
564        assert_ne!(
565            s1.hash_state().unwrap(),
566            s2.hash_state().unwrap(),
567            "off-by-one in a single field must produce a different hash"
568        );
569
570        // Adding a record changes the hash too.
571        let mut s3 = MemoryStore::new();
572        s3.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
573        s3.seed("Caja", Uuid::new_v4(), json!({"id": "extra", "saldo": 0}));
574        assert_ne!(s1.hash_state().unwrap(), s3.hash_state().unwrap());
575    }
576
577    #[test]
578    fn last_applied_seq_round_trips_and_resets_on_clear() {
579        let mut store = MemoryStore::new();
580        assert_eq!(
581            store.last_applied_seq().unwrap(),
582            None,
583            "fresh MemoryStore has no marker"
584        );
585        store.set_last_applied_seq(5).unwrap();
586        assert_eq!(store.last_applied_seq().unwrap(), Some(5));
587        store.set_last_applied_seq(12).unwrap();
588        assert_eq!(store.last_applied_seq().unwrap(), Some(12));
589        store.clear().unwrap();
590        assert_eq!(
591            store.last_applied_seq().unwrap(),
592            None,
593            "clear() resets the marker — a cleared store has applied nothing"
594        );
595    }
596
597    #[test]
598    fn integer_and_integer_valued_float_hash_identically() {
599        // The cross-backend property: the same numeric value, written
600        // by a backend as i64 vs read back as integer-valued f64,
601        // must hash the same.
602        let int_value = json!({"saldo": 100_i64});
603        let float_value = json!({"saldo": 100.0_f64});
604
605        let mut h_int = sha2::Sha256::new();
606        super::hash_value(&mut h_int, &int_value);
607        let mut h_float = sha2::Sha256::new();
608        super::hash_value(&mut h_float, &float_value);
609        assert_eq!(
610            h_int.finalize(),
611            h_float.finalize(),
612            "integer-valued numbers must canonicalize regardless of source representation"
613        );
614    }
615
616    #[test]
617    fn fractional_floats_do_not_canonicalize_to_int() {
618        // Floats with fractional parts must remain floats — collapsing
619        // 100.5 into 100 would hide real differences.
620        let int_value = json!({"x": 100_i64});
621        let frac_value = json!({"x": 100.5_f64});
622
623        let mut h_int = sha2::Sha256::new();
624        super::hash_value(&mut h_int, &int_value);
625        let mut h_frac = sha2::Sha256::new();
626        super::hash_value(&mut h_frac, &frac_value);
627        assert_ne!(
628            h_int.finalize(),
629            h_frac.finalize(),
630            "100 and 100.5 must hash differently"
631        );
632    }
633
634    #[test]
635    fn same_object_with_different_insertion_order_hashes_same() {
636        // serde_json::Map is BTreeMap by default but we sort defensively
637        // in case `preserve_order` is enabled by some transitive dep.
638        let mut m1 = serde_json::Map::new();
639        m1.insert("a".into(), json!(1));
640        m1.insert("b".into(), json!(2));
641        m1.insert("c".into(), json!(3));
642        let mut m2 = serde_json::Map::new();
643        m2.insert("c".into(), json!(3));
644        m2.insert("a".into(), json!(1));
645        m2.insert("b".into(), json!(2));
646
647        let mut h1 = sha2::Sha256::new();
648        super::hash_value(&mut h1, &Value::Object(m1));
649        let mut h2 = sha2::Sha256::new();
650        super::hash_value(&mut h2, &Value::Object(m2));
651        assert_eq!(h1.finalize(), h2.finalize());
652    }
653
654    #[test]
655    fn type_tagged_framing_distinguishes_string_from_number() {
656        // The string "42" must not collide with the number 42.
657        let str_v = json!("42");
658        let num_v = json!(42);
659        let mut h_str = sha2::Sha256::new();
660        super::hash_value(&mut h_str, &str_v);
661        let mut h_num = sha2::Sha256::new();
662        super::hash_value(&mut h_num, &num_v);
663        assert_ne!(h_str.finalize(), h_num.finalize());
664
665        // Bool true must not collide with the number 1.
666        let bool_v = json!(true);
667        let one_v = json!(1);
668        let mut h_bool = sha2::Sha256::new();
669        super::hash_value(&mut h_bool, &bool_v);
670        let mut h_one = sha2::Sha256::new();
671        super::hash_value(&mut h_one, &one_v);
672        assert_ne!(h_bool.finalize(), h_one.finalize());
673    }
674
675    #[test]
676    fn empty_store_has_a_well_defined_hash() {
677        let s1 = MemoryStore::new();
678        let s2 = MemoryStore::new();
679        assert_eq!(s1.hash_state().unwrap(), s2.hash_state().unwrap());
680        // The empty hash is the SHA-256 of an empty input — fix the
681        // expected bytes so an accidental framing change in `hash_state`
682        // can't silently sail through.
683        let expected = hex_decode(
684            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
685        );
686        assert_eq!(s1.hash_state().unwrap().to_vec(), expected);
687    }
688
689    fn hex_decode(s: &str) -> Vec<u8> {
690        (0..s.len())
691            .step_by(2)
692            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex"))
693            .collect()
694    }
695}