weftgraph 0.1.1

Graph Storage gear: typed, multi-tenant knowledge graph with search and traversal over a pluggable store
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
#![allow(clippy::expect_used, clippy::unwrap_used)]

//! The conformance suite against the in-memory fake.
//!
//! This lane needs no database, so it runs everywhere the workspace builds —
//! which is the point: if an obligation only the `PostgreSQL` store can satisfy
//! sneaks into the contract, it fails here first.

mod conformance;

use graph_storage::infra::fake_store::FakeGraphStore;
use graph_storage_sdk::models::ProjectionRequest;
use uuid::Uuid;

fn store() -> FakeGraphStore {
    FakeGraphStore::new()
}

#[tokio::test]
async fn a_failed_batch_commits_nothing() {
    conformance::batch_atomicity(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn source_generations_are_fenced_monotonically() {
    conformance::generation_fencing(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_node_never_outlives_its_incident_edges() {
    conformance::no_orphan_edges(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_fresh_tenant_reports_a_usable_revision() {
    conformance::a_fresh_tenant_reports_a_usable_revision(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn materializing_a_phantom_revalidates_its_edges() {
    conformance::materializing_a_phantom_revalidates_its_edges(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn an_edge_type_refuses_an_endpoint_it_does_not_admit() {
    conformance::endpoint_constraints_are_enforced(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_recorded_idempotency_key_replays() {
    conformance::idempotency(&store(), Uuid::now_v7()).await;
    conformance::a_keyless_retry_is_a_new_request(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn an_identical_batch_converges_without_moving_the_revision() {
    conformance::convergent_replay(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn an_unchanged_re_ingest_embeds_nothing() {
    conformance::an_unchanged_re_ingest_embeds_nothing(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn colliding_node_keys_stay_inside_their_tenants() {
    conformance::tenant_isolation(&store(), Uuid::now_v7(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn tombstoned_rows_are_absent_from_every_read_path() {
    conformance::tombstones_are_invisible(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_denied_row_reads_exactly_like_an_absent_one() {
    conformance::denied_is_indistinguishable_from_absent(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_denying_scope_ranks_nothing() {
    conformance::search_is_scoped(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_document_is_retrieved_by_its_own_text() {
    conformance::a_document_is_retrieved_by_its_own_text(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_declared_path_reaches_the_vector() {
    conformance::a_declared_path_reaches_the_vector(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_skipped_re_ingest_preserves_the_vector() {
    conformance::a_skipped_re_ingest_preserves_the_vector(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_stale_vector_stops_ranking_but_the_node_stays() {
    conformance::a_stale_vector_stops_ranking_but_the_node_stays(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn only_the_active_epoch_ranks() {
    conformance::only_the_active_epoch_ranks(&store(), Uuid::now_v7()).await;
}

/// Obligation 5, which only the fake can currently satisfy: two arms of one
/// read and a hydration after it observe one graph state.
///
/// The built-in `PostgreSQL` store declares this capability **absent** — a true
/// repeatable-read snapshot needs a transaction held across calls, which the
/// sealed runner cannot express (DESIGN § 3.3). Keeping the case
/// here, against the implementation that does honour it, is what stops the
/// obligation from quietly disappearing from the contract.
#[tokio::test]
async fn one_snapshot_spans_every_arm_of_one_read() {
    use graph_storage_sdk::plugin_api::GraphStoreV1;
    use toolkit_security::AccessScope;

    let store = store();
    let tenant = Uuid::now_v7();
    let scope = AccessScope::for_tenant(tenant);
    let ctx = conformance::ctx(tenant, &scope, None);

    store
        .register_types(&ctx, conformance::ontology_batch())
        .await
        .expect("ontology registers");
    conformance::ingest_batch(
        &store,
        &ctx,
        conformance::batch(vec![conformance::node("snap-1", "before")], Vec::new()),
    )
    .await
    .expect("the batch commits");

    let snapshot = store.begin_read(&ctx).await.expect("snapshot opens");

    // A concurrent commit lands between the arms of the compound read.
    conformance::ingest_batch(
        &store,
        &ctx,
        conformance::batch(vec![conformance::node("snap-2", "after")], Vec::new()),
    )
    .await
    .expect("the concurrent batch commits");

    let under = conformance::ctx(tenant, &scope, Some(&snapshot));
    let page = store
        .project_table(&under, ProjectionRequest::default())
        .await
        .expect("projection succeeds");
    assert!(
        page.items.iter().all(|row| row.node_key != "snap-2"),
        "the snapshot must not see a row committed after it opened: {:?}",
        page.items
    );
    // The platform `Page` has no revision slot, so the compound read's
    // revision is asserted through the arms that do carry it (see
    // DESIGN § Read Consistency Contract).

    let resolved = store
        .resolve_node_ids(&under, &["snap-2".to_owned()])
        .await
        .expect("resolution succeeds");
    assert!(
        resolved.is_empty(),
        "a second arm of the same read observes the same state"
    );

    store.end_read(snapshot).await.expect("snapshot closes");
}

#[tokio::test]
async fn the_envelope_records_the_subject_of_each_verb() {
    conformance::the_envelope_records_the_subject_of_each_verb(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_projection_row_carries_the_envelope() {
    conformance::a_projection_row_carries_the_envelope(&store(), Uuid::now_v7()).await;
}

// --- payload projection (ADR-0003) ----------------------------------

#[tokio::test]
async fn a_declared_payload_path_filters_and_orders_the_projection() {
    conformance::a_declared_payload_path_filters_and_orders_the_projection(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn an_undeclared_payload_path_is_refused_naming_the_alternatives() {
    conformance::an_undeclared_payload_path_is_refused_naming_the_alternatives(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn an_index_path_onto_a_non_scalar_is_refused_at_registration() {
    conformance::an_index_path_onto_a_non_scalar_is_refused_at_registration(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn a_deeper_chain_registers_and_its_ancestor_admits_the_leaf() {
    conformance::a_deeper_chain_registers_and_its_ancestor_admits_the_leaf(
        &store().with_max_chain_depth(8),
        Uuid::now_v7(),
    )
    .await;
}

// --- type evolution (registering a changed schema in place) -----------------

#[tokio::test]
async fn a_backward_compatible_change_updates_the_type_in_place() {
    conformance::a_backward_compatible_change_updates_the_type_in_place(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn an_incompatible_change_is_refused_with_its_location() {
    conformance::an_incompatible_change_is_refused_with_its_location(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn a_changed_schema_is_still_a_conflict_by_default() {
    conformance::a_changed_schema_is_still_a_conflict_by_default(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_dry_run_reports_every_verdict_and_writes_nothing() {
    conformance::a_dry_run_reports_every_verdict_and_writes_nothing(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_change_the_schemas_cannot_prove_is_admitted_when_the_rows_fit() {
    conformance::a_change_the_schemas_cannot_prove_is_admitted_when_the_rows_fit(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn a_change_the_stored_rows_contradict_is_refused_naming_them() {
    conformance::a_change_the_stored_rows_contradict_is_refused_naming_them(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn an_accepted_type_update_advances_the_graph_revision() {
    conformance::an_accepted_type_update_advances_the_graph_revision(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn a_new_index_path_becomes_filterable_without_recreating_the_type() {
    conformance::a_new_index_path_becomes_filterable_without_recreating_the_type(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

// --- payload migrations ------------------------------------------------------

#[tokio::test]
async fn a_migration_moves_the_data_with_the_type() {
    conformance::a_migration_moves_the_data_with_the_type(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_migration_that_leaves_rows_invalid_is_refused_naming_them() {
    conformance::a_migration_that_leaves_rows_invalid_is_refused_naming_them(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn a_migration_without_a_schema_change_is_refused() {
    conformance::a_migration_without_a_schema_change_is_refused(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_migration_stamps_its_writer_and_moves_the_version() {
    conformance::a_migration_stamps_its_writer_and_moves_the_version(&store(), Uuid::now_v7())
        .await;
}

// --- source-namespace ownership ----------------------------------------------

#[tokio::test]
async fn a_source_namespace_is_claimed_by_its_first_writer() {
    conformance::a_source_namespace_is_claimed_by_its_first_writer(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn writing_under_another_producers_namespace_is_forbidden() {
    conformance::writing_under_another_producers_namespace_is_forbidden(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn a_transfer_moves_the_namespace_and_records_who_moved_it() {
    conformance::a_transfer_moves_the_namespace_and_records_who_moved_it(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn an_owned_nodes_source_field_claims_no_namespace() {
    conformance::an_owned_nodes_source_field_claims_no_namespace(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn readiness_reports_every_capability_and_only_some_block_service() {
    conformance::readiness_reports_every_capability_and_only_some_block_service(&store()).await;
}

// --- scope replacement --------------------------------------------------------

#[tokio::test]
async fn scope_replacement_removes_what_the_batch_no_longer_names() {
    conformance::scope_replacement_removes_what_the_batch_no_longer_names(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn a_store_without_labels_refuses_every_label_call() {
    conformance::a_store_without_labels_refuses_every_label_call(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn an_edge_cannot_name_a_tombstoned_endpoint() {
    conformance::an_edge_cannot_name_a_tombstoned_endpoint(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn node_types_answers_the_live_nodes_it_is_asked_about() {
    conformance::node_types_answers_the_live_nodes_it_is_asked_about(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn an_edge_only_scope_drops_the_edges_it_stops_declaring() {
    conformance::an_edge_only_scope_drops_the_edges_it_stops_declaring(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn scope_replacement_removes_an_edge_whose_endpoints_remain() {
    conformance::scope_replacement_removes_an_edge_whose_endpoints_remain(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn an_edge_is_not_taken_from_the_scope_that_declared_it() {
    conformance::an_edge_is_not_taken_from_the_scope_that_declared_it(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn one_replacement_does_not_take_another_scopes_edges() {
    conformance::one_replacement_does_not_take_another_scopes_edges(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn scope_replacement_preserves_analysis_edges_and_their_endpoints() {
    conformance::scope_replacement_preserves_analysis_edges_and_their_endpoints(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

/// Multi-threaded on purpose: the case spawns two tasks, and they can only be
/// inside the store at once if the runtime has threads to put them on. The
/// flavour alone is not enough — two futures joined on one task never run in
/// parallel whatever the flavour — which is why the case spawns.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_replacements_of_one_scope_serialize() {
    conformance::two_replacements_of_one_scope_serialize(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// On this store the ingest takes one lock for the whole call, so the eight
/// writers cannot interleave inside it and the concurrency is structural. What
/// it still proves is the counting: eight updates advance the version eight
/// times. The interleaving itself is exercised in `pg_conformance`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn every_update_of_a_node_advances_its_version() {
    conformance::every_update_of_a_node_advances_its_version(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_writers_with_one_expected_version_do_not_both_win() {
    conformance::two_writers_with_one_expected_version_do_not_both_win(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// Run here too: the in-memory store serializes registrations under one lock,
/// so the race cannot open, and what this holds it to is the same arithmetic --
/// one revision per accepted update.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_type_updates_do_not_share_one_revision() {
    conformance::two_type_updates_do_not_share_one_revision(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// Run here too, so both stores answer a delete racing an upsert the same
/// way. The window itself cannot open in this one -- the lock is held across
/// the whole of `ingest` -- so what this proves for the fake store is that the
/// two orderings either side of the window agree with `PostgreSQL`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_delete_racing_an_upsert_leaves_no_rewritten_tombstone() {
    conformance::a_delete_racing_an_upsert_leaves_no_rewritten_tombstone(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// Run here too. This store holds one lock across the whole of `ingest`, so
/// the replacement's read and its removal cannot be split by another write;
/// what it proves here is that both serial orders keep the moved node.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_replacement_does_not_delete_what_an_ingest_moved_out_of_its_scope() {
    conformance::a_replacement_does_not_delete_what_an_ingest_moved_out_of_its_scope(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// Run here too: whether or not the window opens, a phantom two batches
/// both name must be materialized once and shared.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_batches_naming_one_new_endpoint_both_land() {
    conformance::two_batches_naming_one_new_endpoint_both_land(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// Run here too. This store serializes `soft_delete` under one lock, so the
/// second delete reads the tombstone rather than racing the write -- what it
/// proves here is that both routes answer with the same single tombstone.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_node_delete_racing_its_edge_delete_counts_every_edge_once() {
    conformance::a_node_delete_racing_its_edge_delete_counts_every_edge_once(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_deletes_of_one_node_tombstone_it_once() {
    conformance::two_deletes_of_one_node_tombstone_it_once(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// The edge half, here for the same reason: whether or not the window opens,
/// both stores must revive a deleted edge rather than refuse it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_deleted_edge_is_revived_by_the_next_upsert() {
    conformance::a_deleted_edge_is_revived_by_the_next_upsert(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

/// Spawned and multi-threaded like the case it shares, but the race it
/// describes cannot happen here: this store takes one lock for the whole of
/// `ingest` and holds it across the body, with no `.await` inside, so the
/// eight writers are serialized before they reach the counter. What the case
/// proves against this store is the arithmetic -- eight commits, eight
/// revisions, and a replay that does not move it. The interleaving is
/// exercised in `pg_conformance`, where removing the fix reports five
/// distinct revisions for eight states.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn every_committed_mutation_gets_its_own_revision() {
    conformance::every_committed_mutation_gets_its_own_revision(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

// --- both families, and the edge read -----------------------------------------

#[tokio::test]
async fn both_node_families_and_both_edge_families_round_trip() {
    conformance::both_node_families_and_both_edge_families_round_trip(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn an_edge_read_carries_the_envelope() {
    conformance::an_edge_read_carries_the_envelope(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn an_edge_whose_endpoint_is_hidden_is_not_readable() {
    conformance::an_edge_whose_endpoint_is_hidden_is_not_readable(
        &store(),
        Uuid::now_v7(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn no_read_surface_answers_with_another_tenants_rows() {
    conformance::no_read_surface_answers_with_another_tenants_rows(
        &store(),
        Uuid::now_v7(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn an_edge_type_evolves_over_its_own_rows() {
    conformance::an_edge_type_evolves_over_its_own_rows(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_same_key_ingest_may_not_change_the_type() {
    conformance::a_same_key_ingest_may_not_change_the_type(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn per_item_outcomes_follow_the_batch_order() {
    conformance::per_item_outcomes_follow_the_batch_order(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_scope_and_an_idempotency_key_belong_to_their_producer() {
    conformance::a_scope_and_an_idempotency_key_belong_to_their_producer(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn a_type_pattern_narrows_search_and_a_hop() {
    conformance::a_type_pattern_narrows_search_and_a_hop(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn hybrid_search_fuses_both_arms() {
    conformance::hybrid_search_fuses_both_arms(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn deleting_an_already_tombstoned_row_is_a_no_op() {
    conformance::deleting_an_already_tombstoned_row_is_a_no_op(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn the_type_catalogue_pages_through_its_own_cursor() {
    conformance::the_type_catalogue_pages_through_its_own_cursor(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_deleted_conclusion_stops_pinning_its_endpoint() {
    conformance::a_deleted_conclusion_stops_pinning_its_endpoint(&store(), Uuid::now_v7()).await;
}

#[tokio::test]
async fn a_batch_that_names_one_type_twice_is_refused() {
    conformance::a_batch_that_names_one_type_twice_is_refused(&store(), Uuid::now_v7()).await;
}

/// Run here too. This store serializes `ingest` under one lock, so the second
/// claim reads the first's ownership rather than racing its write -- what it
/// proves here is that both routes answer with one owner and one conflict.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_scopes_racing_to_claim_an_unowned_edge_leave_it_with_one() {
    conformance::two_scopes_racing_to_claim_an_unowned_edge_leave_it_with_one(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn an_edge_does_not_follow_a_node_ingested_under_a_new_key() {
    conformance::an_edge_does_not_follow_a_node_ingested_under_a_new_key(&store(), Uuid::now_v7())
        .await;
}

#[tokio::test]
async fn an_expected_version_on_an_absent_key_is_a_conflict_unless_it_is_zero() {
    conformance::an_expected_version_on_an_absent_key_is_a_conflict_unless_it_is_zero(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn a_batch_with_one_conflicting_type_registers_none_and_names_it() {
    conformance::a_batch_with_one_conflicting_type_registers_none_and_names_it(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

#[tokio::test]
async fn a_replacement_leaves_alone_what_does_not_carry_its_attribute() {
    conformance::a_replacement_leaves_alone_what_does_not_carry_its_attribute(
        &store(),
        Uuid::now_v7(),
    )
    .await;
}

/// Run here too: this store serializes `ingest`, so the second creator reads
/// the first's row and the case proves the arithmetic of one winner.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn two_creators_with_expected_version_zero_do_not_both_win() {
    conformance::two_creators_with_expected_version_zero_do_not_both_win(
        std::sync::Arc::new(store())
            as std::sync::Arc<dyn graph_storage_sdk::plugin_api::GraphStoreV1>,
        Uuid::now_v7(),
    )
    .await;
}