Skip to main content

vantage_vista/mocks/
mock_shell.rs

1//! In-memory `TableShell` for tests and examples.
2
3use async_trait::async_trait;
4use ciborium::Value as CborValue;
5use indexmap::IndexMap;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use vantage_core::Result;
9use vantage_types::Record;
10
11use crate::{
12    build_contained_vista,
13    capabilities::VistaCapabilities,
14    column::Column,
15    contained::ContainedWriteback,
16    metadata::VistaMetadata,
17    reference::{ContainedSpec, Reference},
18    sort::SortDirection,
19    source::TableShell,
20    vista::Vista,
21};
22
23#[derive(Clone)]
24pub struct MockShell {
25    data: Arc<Mutex<IndexMap<String, Record<CborValue>>>>,
26    next_auto_id: Arc<Mutex<i64>>,
27    filters: Arc<Mutex<Vec<(String, CborValue)>>>,
28    order: Arc<Mutex<Option<(String, SortDirection)>>>,
29    search: Arc<Mutex<Option<String>>>,
30    capabilities: VistaCapabilities,
31    metadata: VistaMetadata,
32    /// Per-relation target stores, so `get_ref` can resolve a foreign-key
33    /// reference to another in-memory shell — the mock analogue of a driver's
34    /// `with_one`/`with_many`. Registered via [`Self::with_ref_target`].
35    ref_targets: IndexMap<String, MockShell>,
36    /// While set, every read returns `Err` — the in-memory analogue of a
37    /// source that is temporarily unreachable (a 503). Shared across clones
38    /// (incl. narrowed `get_ref` results) so a test can flip it from any handle.
39    fail_reads: Arc<AtomicBool>,
40}
41
42impl MockShell {
43    pub fn new() -> Self {
44        Self {
45            data: Arc::new(Mutex::new(IndexMap::new())),
46            next_auto_id: Arc::new(Mutex::new(1)),
47            filters: Arc::new(Mutex::new(Vec::new())),
48            order: Arc::new(Mutex::new(None)),
49            search: Arc::new(Mutex::new(None)),
50            capabilities: VistaCapabilities {
51                can_count: true,
52                can_insert: true,
53                can_update: true,
54                can_delete: true,
55                can_order: true,
56                can_search: true,
57                ..VistaCapabilities::default()
58            },
59            metadata: VistaMetadata::new(),
60            ref_targets: IndexMap::new(),
61            fail_reads: Arc::new(AtomicBool::new(false)),
62        }
63    }
64
65    /// Register the target store for a foreign-key relation declared in
66    /// `metadata` (via [`VistaMetadata::with_reference`]). `get_ref(relation,
67    /// row)` then returns this store narrowed by `foreign_key == parent_row[id]`
68    /// — the in-memory analogue of a driver resolving a `with_one`/`with_many`.
69    pub fn with_ref_target(mut self, relation: impl Into<String>, target: MockShell) -> Self {
70        self.ref_targets.insert(relation.into(), target);
71        self
72    }
73
74    /// Flip read failure on/off. While `true`, `list`/`get`/`count` return
75    /// `Err`. Shared across clones (incl. narrowed `get_ref` results) via `Arc`,
76    /// so a handle kept before the shell is registered/boxed can fail the live
77    /// dataset mid-run — simulating a source that goes offline.
78    pub fn set_fail_reads(&self, fail: bool) {
79        self.fail_reads.store(fail, Ordering::SeqCst);
80    }
81
82    /// Clone sharing the data, fail toggle, ref targets, and metadata, but with
83    /// FRESH condition state — so narrowing a `get_ref` result doesn't pollute
84    /// the registered target shell's filters/order/search.
85    fn narrowed_clone(&self) -> Self {
86        Self {
87            data: self.data.clone(),
88            next_auto_id: self.next_auto_id.clone(),
89            filters: Arc::new(Mutex::new(Vec::new())),
90            order: Arc::new(Mutex::new(None)),
91            search: Arc::new(Mutex::new(None)),
92            capabilities: self.capabilities.clone(),
93            metadata: self.metadata.clone(),
94            ref_targets: self.ref_targets.clone(),
95            fail_reads: self.fail_reads.clone(),
96        }
97    }
98
99    pub fn with_capabilities(mut self, capabilities: VistaCapabilities) -> Self {
100        self.capabilities = capabilities;
101        self
102    }
103
104    pub fn with_metadata(mut self, metadata: VistaMetadata) -> Self {
105        self.metadata = metadata;
106        self
107    }
108
109    /// Seed a record with an explicit id.
110    pub fn with_record(self, id: impl Into<String>, record: Record<CborValue>) -> Self {
111        self.data.lock().unwrap().insert(id.into(), record);
112        self
113    }
114
115    // ---- Live dataset mutation ---------------------------------------------
116    //
117    // The store is `Arc<Mutex<…>>`, so a clone of this shell taken *before*
118    // it is boxed into a `Vista` keeps a handle to the same rows. These
119    // by-ref helpers let a test or example mutate the dataset mid-run —
120    // simulating an upstream that changed between reads — and have the next
121    // `list`/`get`/refresh observe it. They are additive and opt-in; an
122    // untouched shell behaves exactly as before.
123
124    /// Insert or replace a record by id through the shared store.
125    pub fn set_record(&self, id: impl Into<String>, record: Record<CborValue>) {
126        self.data.lock().unwrap().insert(id.into(), record);
127    }
128
129    /// Overwrite a single field of an existing record (read-modify-write).
130    /// No-op if the record is absent.
131    pub fn set_field(&self, id: &str, field: &str, value: CborValue) {
132        if let Some(rec) = self.data.lock().unwrap().get_mut(id) {
133            rec.insert(field.to_string(), value);
134        }
135    }
136
137    /// Remove a record by id. No-op if absent.
138    pub fn remove_record(&self, id: &str) {
139        self.data.lock().unwrap().shift_remove(id);
140    }
141
142    /// Drop every record.
143    pub fn clear_records(&self) {
144        self.data.lock().unwrap().clear();
145    }
146
147    /// Number of records currently held. Companion to the live-mutation
148    /// helpers above, for tests/examples that grow or shrink the store at
149    /// runtime and want to assert on its size without an async `list`.
150    pub fn len(&self) -> usize {
151        self.data.lock().unwrap().len()
152    }
153
154    /// Whether the store holds no records.
155    pub fn is_empty(&self) -> bool {
156        self.len() == 0
157    }
158
159    /// Snapshot one record by id, straight off the store — no conditions, no
160    /// `fail_reads` guard. Companion to the live-mutation helpers: an effect
161    /// or test reading what it is about to mutate is inspecting its own
162    /// store, not querying a source.
163    pub fn get_record(&self, id: &str) -> Option<Record<CborValue>> {
164        self.data.lock().unwrap().get(id).cloned()
165    }
166
167    /// Ids currently held, in store order. Same store-side view as
168    /// [`Self::get_record`].
169    pub fn record_ids(&self) -> Vec<String> {
170        self.data.lock().unwrap().keys().cloned().collect()
171    }
172
173    /// Return `Err` while `fail_reads` is set — see [`Self::set_fail_reads`].
174    fn guard_reads(&self) -> Result<()> {
175        if self.fail_reads.load(Ordering::SeqCst) {
176            return Err(vantage_core::error!("mock source read failed (injected)"));
177        }
178        Ok(())
179    }
180
181    fn matches_filters(&self, record: &Record<CborValue>) -> bool {
182        self.filters
183            .lock()
184            .unwrap()
185            .iter()
186            .all(|(field, expected)| record.get(field) == Some(expected))
187    }
188
189    fn matches_search(&self, record: &Record<CborValue>) -> bool {
190        let guard = self.search.lock().unwrap();
191        let Some(needle) = guard.as_deref() else {
192            return true;
193        };
194        let needle_lc = needle.to_lowercase();
195        record.values().any(|v| match v {
196            CborValue::Text(s) => s.to_lowercase().contains(&needle_lc),
197            _ => false,
198        })
199    }
200
201    fn next_auto_id(&self) -> String {
202        let mut next = self.next_auto_id.lock().unwrap();
203        let id = next.to_string();
204        *next += 1;
205        id
206    }
207}
208
209impl Default for MockShell {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215#[async_trait]
216impl TableShell for MockShell {
217    fn columns(&self) -> &IndexMap<String, Column> {
218        &self.metadata.columns
219    }
220
221    fn references(&self) -> &IndexMap<String, Reference> {
222        &self.metadata.references
223    }
224
225    fn contained(&self) -> &IndexMap<String, ContainedSpec> {
226        &self.metadata.contained
227    }
228
229    /// Resolve a contained relation against `row`, with a writeback that patches
230    /// the parent record's host column directly in this mock's store — the
231    /// in-memory analogue of a driver patching its row.
232    fn get_contained_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
233        let spec = self.metadata.contained.get(relation).ok_or_else(|| {
234            vantage_core::error!("unknown contained relation", relation = relation)
235        })?;
236        let host_value = row.get(&spec.host_column).cloned();
237
238        let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
239        let parent_id = match row.get(id_field) {
240            Some(CborValue::Text(s)) => s.clone(),
241            _ => {
242                return Err(vantage_core::error!(
243                    "contained traversal requires the parent row's id",
244                    relation = relation
245                ));
246            }
247        };
248
249        let data = self.data.clone();
250        let host_column = spec.host_column.clone();
251        let writeback: ContainedWriteback = Arc::new(move |collection: CborValue| {
252            let data = data.clone();
253            let host_column = host_column.clone();
254            let parent_id = parent_id.clone();
255            Box::pin(async move {
256                let mut store = data.lock().unwrap();
257                if let Some(record) = store.get_mut(&parent_id) {
258                    record.insert(host_column, collection);
259                }
260                Ok(())
261            })
262        });
263
264        build_contained_vista(spec, host_value.as_ref(), writeback, None)
265    }
266
267    /// Resolve a foreign-key relation to its registered target store, narrowed
268    /// by `foreign_key == parent_row[id]`. This is a pure descriptor operation
269    /// (no read), so it succeeds even while the target's `fail_reads` is set —
270    /// the failure surfaces later, at load time, on the returned Vista.
271    fn get_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
272        let reference = self
273            .metadata
274            .references
275            .get(relation)
276            .ok_or_else(|| vantage_core::error!("unknown relation", relation = relation))?;
277        let target = self.ref_targets.get(relation).ok_or_else(|| {
278            vantage_core::error!("no ref target registered for relation", relation = relation)
279        })?;
280        let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
281        let parent_val = row.get(id_field).cloned().ok_or_else(|| {
282            vantage_core::error!(
283                "parent row missing id for traversal",
284                relation = relation,
285                id_field = id_field
286            )
287        })?;
288        let narrowed = target.narrowed_clone();
289        narrowed
290            .filters
291            .lock()
292            .unwrap()
293            .push((reference.foreign_key.clone(), parent_val));
294        Ok(Vista::new(reference.target.clone(), Box::new(narrowed)))
295    }
296
297    fn id_column(&self) -> Option<&str> {
298        self.metadata.id_column.as_deref()
299    }
300
301    async fn list_vista_values(
302        &self,
303        _vista: &Vista,
304    ) -> Result<IndexMap<String, Record<CborValue>>> {
305        self.guard_reads()?;
306        let data = self.data.lock().unwrap();
307        let mut rows: Vec<(String, Record<CborValue>)> = data
308            .iter()
309            .filter(|(_, record)| self.matches_filters(record) && self.matches_search(record))
310            .map(|(k, v)| (k.clone(), v.clone()))
311            .collect();
312        if let Some((field, dir)) = self.order.lock().unwrap().clone() {
313            rows.sort_by(|a, b| {
314                let lhs = a.1.get(&field);
315                let rhs = b.1.get(&field);
316                let ord = cbor_cmp(lhs, rhs);
317                match dir {
318                    SortDirection::Ascending => ord,
319                    SortDirection::Descending => ord.reverse(),
320                }
321            });
322        }
323        Ok(rows.into_iter().collect())
324    }
325
326    /// Windowed read that honours the shell's current `add_order` — the mock's
327    /// analogue of a driver serving an ordered `[offset, limit)` page. Gated by
328    /// `can_fetch_window` like every driver.
329    ///
330    /// Clones only the returned window. The obvious implementation — list
331    /// everything, then slice — copies the whole store per window, which on a
332    /// large mock (200k rows) turns every "fast" page into a near-second stall
333    /// and made the shaped-faker latency knobs meaningless. Filtering and
334    /// ordering work over references; records are cloned after the slice.
335    async fn fetch_window(
336        &self,
337        _vista: &Vista,
338        offset: usize,
339        limit: usize,
340    ) -> Result<Vec<(String, Record<CborValue>)>> {
341        self.guard_reads()?;
342        let data = self.data.lock().unwrap();
343        let matches =
344            |record: &Record<CborValue>| self.matches_filters(record) && self.matches_search(record);
345        let order = self.order.lock().unwrap().clone();
346        Ok(match order {
347            Some((field, dir)) => {
348                let mut refs: Vec<(&String, &Record<CborValue>)> =
349                    data.iter().filter(|(_, r)| matches(r)).collect();
350                refs.sort_by(|a, b| {
351                    let ord = cbor_cmp(a.1.get(&field), b.1.get(&field));
352                    match dir {
353                        SortDirection::Ascending => ord,
354                        SortDirection::Descending => ord.reverse(),
355                    }
356                });
357                refs.into_iter()
358                    .skip(offset)
359                    .take(limit)
360                    .map(|(k, v)| (k.clone(), v.clone()))
361                    .collect()
362            }
363            None => data
364                .iter()
365                .filter(|(_, r)| matches(r))
366                .skip(offset)
367                .take(limit)
368                .map(|(k, v)| (k.clone(), v.clone()))
369                .collect(),
370        })
371    }
372
373    /// Share the backing store, but give the copy its **own** query state
374    /// (filters / order / search) — the `clone_shell` contract. `MockShell`'s
375    /// derived `Clone` shares those (they're `Arc<Mutex<_>>`), so it can't be
376    /// used here: narrowing the clone (e.g. `add_order` in
377    /// `Dio::fetch_window_ordered`) must not reach back and reorder the original.
378    /// `data` / `next_auto_id` / `fail_reads` stay shared (the store).
379    fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
380        Some(Box::new(MockShell {
381            data: self.data.clone(),
382            next_auto_id: self.next_auto_id.clone(),
383            filters: Arc::new(Mutex::new(self.filters.lock().unwrap().clone())),
384            order: Arc::new(Mutex::new(self.order.lock().unwrap().clone())),
385            search: Arc::new(Mutex::new(self.search.lock().unwrap().clone())),
386            capabilities: self.capabilities.clone(),
387            metadata: self.metadata.clone(),
388            ref_targets: self.ref_targets.clone(),
389            fail_reads: self.fail_reads.clone(),
390        }))
391    }
392
393    async fn get_vista_value(
394        &self,
395        _vista: &Vista,
396        id: &String,
397    ) -> Result<Option<Record<CborValue>>> {
398        self.guard_reads()?;
399        Ok(self.data.lock().unwrap().get(id).cloned())
400    }
401
402    async fn get_vista_some_value(
403        &self,
404        _vista: &Vista,
405    ) -> Result<Option<(String, Record<CborValue>)>> {
406        self.guard_reads()?;
407        let data = self.data.lock().unwrap();
408        Ok(data
409            .iter()
410            .find(|(_, record)| self.matches_filters(record))
411            .map(|(k, v)| (k.clone(), v.clone())))
412    }
413
414    async fn insert_vista_value(
415        &self,
416        _vista: &Vista,
417        id: &String,
418        record: &Record<CborValue>,
419    ) -> Result<Record<CborValue>> {
420        let mut data = self.data.lock().unwrap();
421        if data.contains_key(id) {
422            return Err(vantage_core::error!("Record already exists", id = id));
423        }
424        let mut stored = record.clone();
425        stored.insert("id".to_string(), CborValue::Text(id.clone()));
426        data.insert(id.clone(), stored.clone());
427        Ok(stored)
428    }
429
430    async fn replace_vista_value(
431        &self,
432        _vista: &Vista,
433        id: &String,
434        record: &Record<CborValue>,
435    ) -> Result<Record<CborValue>> {
436        let mut data = self.data.lock().unwrap();
437        let mut stored = record.clone();
438        stored.insert("id".to_string(), CborValue::Text(id.clone()));
439        data.insert(id.clone(), stored.clone());
440        Ok(stored)
441    }
442
443    async fn patch_vista_value(
444        &self,
445        _vista: &Vista,
446        id: &String,
447        partial: &Record<CborValue>,
448    ) -> Result<Record<CborValue>> {
449        let mut data = self.data.lock().unwrap();
450        let existing = data
451            .get_mut(id)
452            .ok_or_else(|| vantage_core::error!("Record not found", id = id))?;
453        for (k, v) in partial {
454            existing.insert(k.clone(), v.clone());
455        }
456        Ok(existing.clone())
457    }
458
459    async fn delete_vista_value(&self, _vista: &Vista, id: &String) -> Result<()> {
460        let mut data = self.data.lock().unwrap();
461        if data.shift_remove(id).is_none() {
462            Err(vantage_core::error!("Record not found", id = id))
463        } else {
464            Ok(())
465        }
466    }
467
468    async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
469        self.data.lock().unwrap().clear();
470        Ok(())
471    }
472
473    async fn insert_vista_return_id_value(
474        &self,
475        vista: &Vista,
476        record: &Record<CborValue>,
477    ) -> Result<String> {
478        let id = match record.get("id") {
479            Some(CborValue::Text(s)) if !s.is_empty() => s.clone(),
480            Some(CborValue::Integer(i)) => i128::from(*i).to_string(),
481            _ => self.next_auto_id(),
482        };
483        self.insert_vista_value(vista, &id, record).await?;
484        Ok(id)
485    }
486
487    /// Count without materializing: `list_vista_values` clones every matching
488    /// record just to take a length, which turns each counted window fetch on
489    /// a large store into a full-store copy (1.8 s against a 200k-row mock).
490    /// Counting only needs the predicates.
491    async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
492        let _ = vista;
493        self.guard_reads()?;
494        let data = self.data.lock().unwrap();
495        Ok(data
496            .values()
497            .filter(|record| self.matches_filters(record) && self.matches_search(record))
498            .count() as i64)
499    }
500
501    fn capabilities(&self) -> &VistaCapabilities {
502        &self.capabilities
503    }
504
505    fn driver_name(&self) -> &'static str {
506        "mock"
507    }
508
509    fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
510        self.filters
511            .lock()
512            .unwrap()
513            .push((field.to_string(), value.clone()));
514        Ok(())
515    }
516
517    fn add_order(&mut self, field: &str, dir: SortDirection) -> Result<()> {
518        *self.order.lock().unwrap() = Some((field.to_string(), dir));
519        Ok(())
520    }
521
522    fn clear_orders(&mut self) -> Result<()> {
523        *self.order.lock().unwrap() = None;
524        Ok(())
525    }
526
527    fn add_search(&mut self, text: &str) -> Result<()> {
528        *self.search.lock().unwrap() = Some(text.to_string());
529        Ok(())
530    }
531
532    fn clear_search(&mut self) -> Result<()> {
533        *self.search.lock().unwrap() = None;
534        Ok(())
535    }
536}
537
538/// Total-order comparator for the CBOR scalars MockShell records carry.
539/// Falls back to lexical ordering of CBOR-as-text for mixed-or-unknown
540/// types, which keeps sort deterministic without claiming semantic
541/// equivalence between heterogeneous values.
542fn cbor_cmp(a: Option<&CborValue>, b: Option<&CborValue>) -> std::cmp::Ordering {
543    use std::cmp::Ordering;
544    match (a, b) {
545        (None, None) => Ordering::Equal,
546        (None, _) => Ordering::Less,
547        (_, None) => Ordering::Greater,
548        (Some(lhs), Some(rhs)) => match (lhs, rhs) {
549            (CborValue::Text(l), CborValue::Text(r)) => l.cmp(r),
550            (CborValue::Integer(l), CborValue::Integer(r)) => i128::from(*l).cmp(&i128::from(*r)),
551            (CborValue::Bool(l), CborValue::Bool(r)) => l.cmp(r),
552            _ => format!("{lhs:?}").cmp(&format!("{rhs:?}")),
553        },
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use crate::{Column, Reference, ReferenceKind, Vista, VistaMetadata};
561    use vantage_dataset::{InsertableValueSet, ReadableValueSet, WritableValueSet};
562
563    fn cbor_text(s: &str) -> CborValue {
564        CborValue::Text(s.into())
565    }
566
567    fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
568        let mut r = Record::new();
569        for (k, v) in pairs {
570            r.insert((*k).to_string(), v.clone());
571        }
572        r
573    }
574
575    fn build_user_vista(source: MockShell) -> Vista {
576        let metadata = VistaMetadata::new()
577            .with_column(Column::new("id", "String").with_flag("id"))
578            .with_column(Column::new("name", "String").with_flag("title"))
579            .with_column(Column::new("email", "String").hidden())
580            .with_column(Column::new("vip_flag", "bool"))
581            .with_id_column("id")
582            .with_reference(Reference::new(
583                "orders",
584                "orders",
585                ReferenceKind::HasMany,
586                "user_id",
587            ));
588        Vista::new("users", Box::new(source.with_metadata(metadata)))
589    }
590
591    #[test]
592    fn metadata_accessors_round_trip() {
593        let vista = build_user_vista(MockShell::new());
594
595        assert_eq!(vista.name(), "users");
596        assert_eq!(vista.get_id_column(), Some("id"));
597        assert_eq!(vista.get_title_columns(), vec!["name"]);
598        assert_eq!(
599            vista.get_column_names(),
600            vec!["id", "name", "email", "vip_flag"]
601        );
602        assert!(vista.get_column("email").unwrap().is_hidden());
603        assert!(!vista.get_column("name").unwrap().is_hidden());
604        assert_eq!(vista.get_references(), vec!["orders".to_string()]);
605        assert_eq!(
606            vista.get_reference("orders").unwrap().foreign_key,
607            "user_id"
608        );
609
610        let caps = vista.capabilities();
611        assert!(caps.can_count && caps.can_insert && caps.can_update && caps.can_delete);
612        assert!(!caps.can_subscribe);
613    }
614
615    #[tokio::test]
616    async fn list_values_returns_seeded_rows() {
617        let source = MockShell::new()
618            .with_record(
619                "1",
620                record(&[("id", cbor_text("1")), ("name", cbor_text("Alice"))]),
621            )
622            .with_record(
623                "2",
624                record(&[("id", cbor_text("2")), ("name", cbor_text("Bob"))]),
625            );
626        let vista = build_user_vista(source);
627
628        let rows = vista.list_values().await.unwrap();
629        assert_eq!(rows.len(), 2);
630        assert!(rows.contains_key("1"));
631        assert_eq!(rows["2"].get("name"), Some(&cbor_text("Bob")));
632
633        let alice = vista.get_value("1").await.unwrap().unwrap();
634        assert_eq!(alice.get("name"), Some(&cbor_text("Alice")));
635
636        assert_eq!(vista.get_count().await.unwrap(), 2);
637    }
638
639    #[tokio::test]
640    async fn add_condition_eq_filters_list_and_count() {
641        let source = MockShell::new()
642            .with_record(
643                "1",
644                record(&[
645                    ("name", cbor_text("Alice")),
646                    ("vip_flag", CborValue::Bool(true)),
647                ]),
648            )
649            .with_record(
650                "2",
651                record(&[
652                    ("name", cbor_text("Bob")),
653                    ("vip_flag", CborValue::Bool(false)),
654                ]),
655            )
656            .with_record(
657                "3",
658                record(&[
659                    ("name", cbor_text("Carol")),
660                    ("vip_flag", CborValue::Bool(true)),
661                ]),
662            );
663        let mut vista = build_user_vista(source);
664        vista
665            .add_condition_eq("vip_flag", CborValue::Bool(true))
666            .unwrap();
667
668        let rows = vista.list_values().await.unwrap();
669        assert_eq!(rows.len(), 2);
670        assert!(rows.contains_key("1"));
671        assert!(rows.contains_key("3"));
672        assert_eq!(vista.get_count().await.unwrap(), 2);
673    }
674
675    #[tokio::test]
676    async fn writable_value_set_round_trip() {
677        let vista = build_user_vista(MockShell::new());
678
679        // insert_value with explicit id
680        let inserted = vista
681            .insert_value("alice", &record(&[("name", cbor_text("Alice"))]))
682            .await
683            .unwrap();
684        assert_eq!(inserted.get("id"), Some(&cbor_text("alice")));
685
686        // duplicate insert_value fails
687        let dup = vista.insert_value("alice", &record(&[])).await;
688        assert!(dup.is_err());
689
690        // replace_value upserts
691        vista
692            .replace_value("alice", &record(&[("name", cbor_text("Alicia"))]))
693            .await
694            .unwrap();
695        let renamed = vista.get_value("alice").await.unwrap().unwrap();
696        assert_eq!(renamed.get("name"), Some(&cbor_text("Alicia")));
697
698        // patch_value merges
699        vista
700            .patch_value(
701                "alice",
702                &record(&[("email", cbor_text("alice@example.com"))]),
703            )
704            .await
705            .unwrap();
706        let patched = vista.get_value("alice").await.unwrap().unwrap();
707        assert_eq!(patched.get("name"), Some(&cbor_text("Alicia")));
708        assert_eq!(patched.get("email"), Some(&cbor_text("alice@example.com")));
709
710        // delete
711        vista.delete("alice").await.unwrap();
712        assert!(vista.get_value("alice").await.unwrap().is_none());
713
714        // delete_all
715        vista
716            .insert_value("a", &record(&[("name", cbor_text("A"))]))
717            .await
718            .unwrap();
719        vista
720            .insert_value("b", &record(&[("name", cbor_text("B"))]))
721            .await
722            .unwrap();
723        vista.delete_all().await.unwrap();
724        assert_eq!(vista.list_values().await.unwrap().len(), 0);
725    }
726
727    #[tokio::test]
728    async fn default_get_value_with_row_ignores_row_and_delegates() {
729        // A driver that does not override `get_vista_value_with_row` must behave
730        // exactly like `get_value` — the extra `row` is ignored.
731        let source = MockShell::new().with_record(
732            "x",
733            record(&[("id", cbor_text("x")), ("name", cbor_text("Xavier"))]),
734        );
735        let vista = build_user_vista(source);
736
737        let mut row: Record<CborValue> = Record::new();
738        row.insert("extra".into(), cbor_text("ignored"));
739
740        let got = vista.get_value_with_row("x", &row).await.unwrap().unwrap();
741        assert_eq!(got.get("name"), Some(&cbor_text("Xavier")));
742    }
743
744    #[tokio::test]
745    async fn insertable_value_set_assigns_ids() {
746        let vista = build_user_vista(MockShell::new());
747
748        // record without id → mock generates one
749        let auto_id = vista
750            .insert_return_id_value(&record(&[("name", cbor_text("Bob"))]))
751            .await
752            .unwrap();
753        assert_eq!(auto_id, "1");
754
755        // record with explicit string id → preserved
756        let explicit = vista
757            .insert_return_id_value(&record(&[
758                ("id", cbor_text("alice")),
759                ("name", cbor_text("Alice")),
760            ]))
761            .await
762            .unwrap();
763        assert_eq!(explicit, "alice");
764
765        assert_eq!(vista.get_count().await.unwrap(), 2);
766    }
767}