@feltdb/core 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +27 -1
  2. package/dist/cli/commands.js +64 -28
  3. package/dist/cli/index.js +1 -1
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +2 -1
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +84 -0
  8. package/dist/create/server-source/crates/feltdb/src/lib.rs +38 -20
  9. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +292 -0
  10. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1856 -0
  11. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +43 -11
  12. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +634 -0
  13. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +366 -0
  14. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +270 -0
  15. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +13 -0
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +194 -9
  17. package/dist/db.d.ts +7 -0
  18. package/dist/db.d.ts.map +1 -1
  19. package/dist/db.js +12 -1
  20. package/dist/embedded-transaction.d.ts.map +1 -1
  21. package/dist/embedded-transaction.js +14 -8
  22. package/dist/feltdb.d.ts +2 -0
  23. package/dist/feltdb.d.ts.map +1 -1
  24. package/dist/http-db.d.ts +24 -0
  25. package/dist/http-db.d.ts.map +1 -1
  26. package/dist/http-db.js +13 -0
  27. package/dist/index-core.d.ts +1 -0
  28. package/dist/index-core.d.ts.map +1 -1
  29. package/dist/studio-app/assets/{feltdb_wasm-CD744e5D.js → feltdb_wasm-DVKsw75S.js} +1 -1
  30. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  31. package/dist/studio-app/assets/{index-DoROs8yx.js → index-C71X92EK.js} +2 -2
  32. package/dist/studio-app/index.html +1 -1
  33. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  34. package/package.json +1 -1
  35. package/dist/studio-app/assets/feltdb_wasm_bg-CiIXhOLi.wasm +0 -0
@@ -0,0 +1,366 @@
1
+ //! Integration Smoke Test: End-to-End State Model Functionality
2
+ //!
3
+ //! This test demonstrates complete state model usage without Git dependencies:
4
+ //! 1. Create initial state
5
+ //! 2. Branch state
6
+ //! 3. Make changes on each branch
7
+ //! 4. Inspect topology
8
+ //! 5. Compute semantic diffs
9
+ //! 6. Classify conflicts
10
+ //! 7. Supply reconciliation result
11
+ //! 8. Materialize reconciled state
12
+ //! 9. Verify restart recovery
13
+
14
+ #[cfg(test)]
15
+ mod integration_smoke_test {
16
+ use feltdb::{
17
+ StateRevision, StateStore, StateTopology, Relationship, SemanticDiff,
18
+ ConflictClassification, ConflictClass, ReconciliationPlan,
19
+ };
20
+ use serde_json::json;
21
+
22
+ #[test]
23
+ fn end_to_end_state_model_smoke_test() {
24
+ // Step 1: Create initial application state
25
+ let store = StateStore::new_volatile();
26
+ let initial_state = store
27
+ .create(
28
+ r#"{"users":{"alice":{"balance":100},"bob":{"balance":50}}}"#.to_string(),
29
+ "initial-authority".to_string(),
30
+ )
31
+ .expect("Failed to create initial state");
32
+
33
+ assert!(store.exists(&initial_state.id));
34
+ assert_eq!(store.current().unwrap().id, initial_state.id);
35
+
36
+ println!("✓ Step 1: Created initial state ({})", initial_state.id);
37
+
38
+ // Step 2: Create first branch (Alice's branch - modifies alice balance)
39
+ let alice_branch = store
40
+ .commit(
41
+ r#"{"users":{"alice":{"balance":150},"bob":{"balance":50}}}"#.to_string(),
42
+ &initial_state,
43
+ "alice-authority".to_string(),
44
+ )
45
+ .expect("Failed to create alice branch");
46
+
47
+ store
48
+ .create_branch("alice-branch".to_string(), alice_branch.id.clone())
49
+ .expect("Failed to create alice-branch");
50
+
51
+ println!(
52
+ "✓ Step 2: Created alice branch ({}), parent: {}",
53
+ alice_branch.id, alice_branch.parent().unwrap()
54
+ );
55
+
56
+ // Step 3: Create second branch from initial state (Bob's branch - modifies bob balance)
57
+ let bob_branch = store
58
+ .commit(
59
+ r#"{"users":{"alice":{"balance":100},"bob":{"balance":150}}}"#.to_string(),
60
+ &initial_state,
61
+ "bob-authority".to_string(),
62
+ )
63
+ .expect("Failed to create bob branch");
64
+
65
+ store
66
+ .create_branch("bob-branch".to_string(), bob_branch.id.clone())
67
+ .expect("Failed to create bob-branch");
68
+
69
+ println!(
70
+ "✓ Step 3: Created bob branch ({}), parent: {}",
71
+ bob_branch.id, bob_branch.parent().unwrap()
72
+ );
73
+
74
+ // Step 4: Inspect topology
75
+ let mut topology = StateTopology::new();
76
+ topology.add_revision(initial_state.clone());
77
+ topology.add_revision(alice_branch.clone());
78
+ topology.add_revision(bob_branch.clone());
79
+
80
+ // Verify ancestry relationships
81
+ assert!(topology.is_ancestor(&initial_state.id, &alice_branch.id));
82
+ assert!(topology.is_ancestor(&initial_state.id, &bob_branch.id));
83
+ assert!(!topology.is_ancestor(&alice_branch.id, &bob_branch.id));
84
+ assert!(!topology.is_ancestor(&bob_branch.id, &alice_branch.id));
85
+
86
+ let common = topology.common_ancestor(&alice_branch.id, &bob_branch.id);
87
+ assert_eq!(common, Some(initial_state.id.clone()));
88
+
89
+ let relationship = topology.relationship(&alice_branch.id, &bob_branch.id);
90
+ assert_eq!(relationship, Relationship::Diverged);
91
+
92
+ println!("✓ Step 4: Verified topology");
93
+ println!(
94
+ " - Alice and Bob branches diverged from {}",
95
+ initial_state.id
96
+ );
97
+
98
+ // Step 5: Compute semantic diffs
99
+ let initial_json = serde_json::from_str(
100
+ r#"{"users":{"alice":{"balance":100},"bob":{"balance":50}}}"#,
101
+ )
102
+ .unwrap();
103
+ let alice_json = serde_json::from_str(
104
+ r#"{"users":{"alice":{"balance":150},"bob":{"balance":50}}}"#,
105
+ )
106
+ .unwrap();
107
+ let bob_json = serde_json::from_str(
108
+ r#"{"users":{"alice":{"balance":100},"bob":{"balance":150}}}"#,
109
+ )
110
+ .unwrap();
111
+
112
+ let alice_diff = SemanticDiff::compute(&initial_json, &alice_json);
113
+ let bob_diff = SemanticDiff::compute(&initial_json, &bob_json);
114
+
115
+ assert_eq!(alice_diff.changes.len(), 1);
116
+ assert_eq!(bob_diff.changes.len(), 1);
117
+
118
+ println!("✓ Step 5: Computed semantic diffs");
119
+ println!(" - Alice's change: {:?}", alice_diff.changes[0]);
120
+ println!(" - Bob's change: {:?}", bob_diff.changes[0]);
121
+
122
+ // Step 6: Classify conflicts
123
+ let conflict_class = ConflictClassification::classify(
124
+ &initial_state,
125
+ &alice_branch,
126
+ &bob_branch,
127
+ );
128
+
129
+ // These changes are independent (different paths)
130
+ assert_eq!(conflict_class.overall, ConflictClass::Independent);
131
+ assert!(
132
+ conflict_class.path_conflicts.iter()
133
+ .all(|c| c.classification == ConflictClass::Independent)
134
+ );
135
+
136
+ println!("✓ Step 6: Classified conflicts as INDEPENDENT");
137
+
138
+ // Step 7: Create reconciliation plan with explicit parent choice
139
+ let plan = ReconciliationPlan::new(
140
+ alice_branch.id.clone(),
141
+ bob_branch.id.clone(),
142
+ initial_state.id.clone(),
143
+ true, // Choose alice's state as parent
144
+ );
145
+
146
+ assert!(plan.validate());
147
+
148
+ println!("✓ Step 7: Created reconciliation plan (parent_choice=alice)");
149
+
150
+ // Step 8: Materialize reconciled state
151
+ // For this test, merge by combining both changes
152
+ let merged_json = json!({
153
+ "users": {
154
+ "alice": {"balance": 150},
155
+ "bob": {"balance": 150}
156
+ }
157
+ });
158
+ let merged_content = merged_json.to_string();
159
+
160
+ let reconciled = StateRevision::child(
161
+ merged_content,
162
+ &alice_branch,
163
+ "reconciliation-authority".to_string(),
164
+ );
165
+
166
+ // Store the reconciled state
167
+ let final_state = store
168
+ .commit(
169
+ reconciled.content.clone(),
170
+ &alice_branch,
171
+ "reconciliation-authority".to_string(),
172
+ )
173
+ .expect("Failed to commit reconciled state");
174
+
175
+ println!("✓ Step 8: Materialized reconciled state ({})", final_state.id);
176
+
177
+ // Step 9: Verify state properties
178
+ let retrieved = store
179
+ .get(&final_state.id)
180
+ .expect("Failed to retrieve final state");
181
+
182
+ assert_eq!(retrieved.id, final_state.id);
183
+ assert_eq!(retrieved.authority, "reconciliation-authority");
184
+ assert!(retrieved.verify_integrity());
185
+
186
+ // Verify we can recover parent
187
+ let parent = store
188
+ .get(retrieved.parent().unwrap())
189
+ .expect("Failed to get parent");
190
+ assert_eq!(parent.id, alice_branch.id);
191
+
192
+ println!("✓ Step 9: Verified state persistence and recovery");
193
+
194
+ // Summary
195
+ println!("\n✅ END-TO-END STATE MODEL TEST PASSED");
196
+ println!(" - Created and branched states");
197
+ println!(" - Verified causal topology");
198
+ println!(" - Computed semantic diffs");
199
+ println!(" - Classified conflicts");
200
+ println!(" - Applied explicit reconciliation");
201
+ println!(" - Verified persistence and recovery");
202
+ }
203
+
204
+ #[test]
205
+ fn test_convergent_conflict_detection() {
206
+ // Test where both branches make the same change (convergent)
207
+ let store = StateStore::new_volatile();
208
+ let initial = store
209
+ .create(
210
+ r#"{"status":"initial"}"#.to_string(),
211
+ "auth".to_string(),
212
+ )
213
+ .unwrap();
214
+
215
+ let both_updated = r#"{"status":"updated"}"#.to_string();
216
+
217
+ let left = store
218
+ .commit(both_updated.clone(), &initial, "auth".to_string())
219
+ .unwrap();
220
+
221
+ let right = StateRevision::child(both_updated, &initial, "auth".to_string());
222
+
223
+ let classification = ConflictClassification::classify(&initial, &left, &right);
224
+ assert_eq!(classification.overall, ConflictClass::Convergent);
225
+
226
+ println!("✓ Convergent conflict correctly detected");
227
+ }
228
+
229
+ #[test]
230
+ fn test_actual_conflict_detection() {
231
+ // Test where both branches make conflicting changes (conflict)
232
+ let store = StateStore::new_volatile();
233
+ let initial = store
234
+ .create(
235
+ r#"{"value":"initial"}"#.to_string(),
236
+ "auth".to_string(),
237
+ )
238
+ .unwrap();
239
+
240
+ let left = store
241
+ .commit(
242
+ r#"{"value":"left-version"}"#.to_string(),
243
+ &initial,
244
+ "auth".to_string(),
245
+ )
246
+ .unwrap();
247
+
248
+ let right = StateRevision::child(
249
+ r#"{"value":"right-version"}"#.to_string(),
250
+ &initial,
251
+ "auth".to_string(),
252
+ );
253
+
254
+ let classification = ConflictClassification::classify(&initial, &left, &right);
255
+ assert_eq!(classification.overall, ConflictClass::Conflict);
256
+
257
+ println!("✓ Actual conflict correctly detected");
258
+ }
259
+
260
+ #[test]
261
+ fn test_unrelated_histories() {
262
+ // Test that unrelated branches are detected as such
263
+ let store1 = StateStore::new_volatile();
264
+ let state1 = store1
265
+ .create(r#"{"id":1}"#.to_string(), "auth1".to_string())
266
+ .unwrap();
267
+
268
+ let store2 = StateStore::new_volatile();
269
+ let state2 = store2
270
+ .create(r#"{"id":2}"#.to_string(), "auth2".to_string())
271
+ .unwrap();
272
+
273
+ let mut topology = StateTopology::new();
274
+ topology.add_revision(state1.clone());
275
+ topology.add_revision(state2.clone());
276
+
277
+ assert_eq!(
278
+ topology.relationship(&state1.id, &state2.id),
279
+ Relationship::Unrelated
280
+ );
281
+ assert!(topology.common_ancestor(&state1.id, &state2.id).is_none());
282
+
283
+ println!("✓ Unrelated histories correctly identified");
284
+ }
285
+
286
+ #[test]
287
+ fn test_representation_sensitivity_in_diff() {
288
+ // Verify that representation-sensitive changes are tracked
289
+ let old = json!({"count": 1});
290
+ let new_int = json!({"count": 1}); // Same numeric value
291
+ let new_string = json!({"count": "1"}); // Different representation
292
+
293
+ let diff1 = SemanticDiff::compute(&old, &new_int);
294
+ let diff2 = SemanticDiff::compute(&old, &new_string);
295
+
296
+ assert_eq!(diff1.changes.len(), 0, "No change for same representation");
297
+ assert_eq!(diff2.changes.len(), 1, "Change detected for different representation");
298
+
299
+ println!("✓ Representation sensitivity verified in diffs");
300
+ }
301
+
302
+ #[test]
303
+ fn test_branching_does_not_advance_current() {
304
+ // Verify that creating a branch doesn't automatically advance current pointer
305
+ let store = StateStore::new_volatile();
306
+ let state1 = store
307
+ .create(r#"{"v":1}"#.to_string(), "auth".to_string())
308
+ .unwrap();
309
+
310
+ let current_after_create = store.current().unwrap();
311
+ assert_eq!(current_after_create.id, state1.id);
312
+
313
+ // Branch from state1
314
+ let state2 = store
315
+ .commit(r#"{"v":2}"#.to_string(), &state1, "auth".to_string())
316
+ .unwrap();
317
+
318
+ store
319
+ .create_branch("test-branch".to_string(), state2.id.clone())
320
+ .unwrap();
321
+
322
+ // Current should still be state2 (because we committed it)
323
+ let current_after_branch = store.current().unwrap();
324
+ assert_eq!(current_after_branch.id, state2.id);
325
+
326
+ println!("✓ Branch creation verified (doesn't implicitly advance current)");
327
+ }
328
+
329
+ #[test]
330
+ fn test_no_automatic_merge() {
331
+ // Verify that reconciliation requires explicit plan
332
+ let store = StateStore::new_volatile();
333
+ let initial = store
334
+ .create(r#"{"a":1,"b":2}"#.to_string(), "auth".to_string())
335
+ .unwrap();
336
+
337
+ let left = store
338
+ .commit(r#"{"a":10,"b":2}"#.to_string(), &initial, "auth".to_string())
339
+ .unwrap();
340
+
341
+ let right = StateRevision::child(
342
+ r#"{"a":1,"b":20}"#.to_string(),
343
+ &initial,
344
+ "auth".to_string(),
345
+ );
346
+
347
+ // Classify but don't automatically merge
348
+ let classification = ConflictClassification::classify(&initial, &left, &right);
349
+
350
+ // Caller must supply explicit plan
351
+ let plan = ReconciliationPlan::new(
352
+ left.id.clone(),
353
+ right.id.clone(),
354
+ initial.id.clone(),
355
+ true,
356
+ );
357
+
358
+ assert!(plan.validate());
359
+ assert!(
360
+ plan.path_overrides.is_empty(),
361
+ "Plan starts empty; caller must supply overrides"
362
+ );
363
+
364
+ println!("✓ No automatic merge; explicit plan required");
365
+ }
366
+ }
@@ -0,0 +1,270 @@
1
+ //! Comprehensive tests for FeltDB State Model persistence
2
+ //!
3
+ //! This test suite verifies that:
4
+ //! 1. State revisions persist to disk through FeltDB
5
+ //! 2. Current pointer persists correctly
6
+ //! 3. Branches persist and recover
7
+ //! 4. Restart recovery works (close database, reopen and recover state)
8
+ //! 5. Deterministic StateIds survive restart
9
+ //! 6. Volatile stores remain unchanged
10
+
11
+ #[cfg(test)]
12
+ mod state_persistence_tests {
13
+ use feltdb::state_model::StateStore;
14
+ use std::sync::Arc;
15
+ use tempfile::TempDir;
16
+
17
+ #[test]
18
+ fn test_persistent_store_saves_revisions_to_disk() {
19
+ let temp_dir = TempDir::new().unwrap();
20
+ let temp_path = temp_dir.path().join("test.log");
21
+
22
+ // Create initial state with FeltDB persistence
23
+ let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
24
+ let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
25
+ let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
26
+ let rev2 = store1.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit");
27
+
28
+ let id1 = rev1.id.clone();
29
+ let id2 = rev2.id.clone();
30
+ let id2_hex = id2.as_hex().to_string();
31
+
32
+ // Verify both revisions exist
33
+ assert!(store1.exists(&id1), "rev1 should exist");
34
+ assert!(store1.exists(&id2), "rev2 should exist");
35
+ assert_eq!(store1.current().map(|r| r.id.as_hex().to_string()), Some(id2_hex.clone()));
36
+
37
+ // Drop store and database (simulate restart)
38
+ drop(store1);
39
+
40
+ // New process: recover from disk
41
+ let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
42
+ let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery setup");
43
+
44
+ // Verify all revisions still exist
45
+ assert!(store2.exists(&id1), "rev1 should survive restart");
46
+ assert!(store2.exists(&id2), "rev2 should survive restart");
47
+ assert_eq!(store2.current().map(|r| r.id.as_hex().to_string()), Some(id2_hex), "current should be restored");
48
+
49
+ // Verify content is correct
50
+ let recovered_rev2 = store2.get(&id2).expect("get rev2");
51
+ assert_eq!(recovered_rev2.content, r#"{"v":2}"#);
52
+ assert_eq!(recovered_rev2.authority, "auth");
53
+ }
54
+
55
+ #[test]
56
+ fn test_persistent_store_preserves_state_id_determinism() {
57
+ let temp_dir = TempDir::new().unwrap();
58
+ let temp_path = temp_dir.path().join("test.log");
59
+
60
+ let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
61
+ let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
62
+ let rev1 = store1.create(r#"{"fixed":"content"}"#.to_string(), "auth".to_string()).expect("create");
63
+ let id1 = rev1.id.clone();
64
+ drop(store1);
65
+
66
+ // Recover and recreate the same content
67
+ let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
68
+ let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
69
+ let recovered = store2.get(&id1).expect("get");
70
+
71
+ // Content hash must be the same for identical content
72
+ assert_eq!(recovered.id.as_hex(), id1.as_hex(), "StateId should be deterministic");
73
+ }
74
+
75
+ #[test]
76
+ fn test_persistent_store_handles_branch_persistence() {
77
+ let temp_dir = TempDir::new().unwrap();
78
+ let temp_path = temp_dir.path().join("test.log");
79
+
80
+ // Create store and initial state
81
+ let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
82
+ let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
83
+ let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
84
+
85
+ let id1 = rev1.id.clone();
86
+ let id1_hex = id1.as_hex().to_string();
87
+
88
+ // Create a branch
89
+ store1.create_branch("main".to_string(), id1.clone()).expect("branch");
90
+
91
+ // Verify branch exists
92
+ assert_eq!(store1.branch_head("main").map(|id| id.as_hex().to_string()), Some(id1_hex.clone()));
93
+ drop(store1);
94
+
95
+ // Recover and check branch
96
+ let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
97
+ let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
98
+ assert_eq!(store2.branch_head("main").map(|id| id.as_hex().to_string()), Some(id1_hex), "branch should survive restart");
99
+ }
100
+
101
+ #[test]
102
+ fn test_volatile_store_does_not_persist() {
103
+ let temp_dir = TempDir::new().unwrap();
104
+ let temp_path = temp_dir.path().join("test.log");
105
+
106
+ // Create volatile store
107
+ let store1 = StateStore::new_volatile();
108
+ let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
109
+ drop(store1);
110
+
111
+ // Try to recover from disk (should be empty)
112
+ let db2 = feltdb::FeltDb::open(&temp_path).expect("open db");
113
+ let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("create store");
114
+ assert!(!store2.exists(&rev1.id), "volatile store should not persist");
115
+ assert!(store2.current().is_none(), "should have no current state");
116
+ }
117
+
118
+ #[test]
119
+ fn test_persistent_store_tracks_ancestry_across_restart() {
120
+ let temp_dir = TempDir::new().unwrap();
121
+ let temp_path = temp_dir.path().join("test.log");
122
+
123
+ // Create chain of revisions
124
+ let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
125
+ let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
126
+ let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
127
+ let rev2 = store1.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit");
128
+ let rev3 = store1.commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string()).expect("commit");
129
+
130
+ let id1 = rev1.id.clone();
131
+ let id2 = rev2.id.clone();
132
+ let id3 = rev3.id.clone();
133
+ drop(store1);
134
+
135
+ // Recover and verify parent pointers
136
+ let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
137
+ let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
138
+
139
+ let recovered_rev3 = store2.get(&id3).expect("get rev3");
140
+ assert_eq!(recovered_rev3.parent_id.as_ref().map(|id| id.as_hex()), Some(id2.as_hex()), "rev3 parent should be rev2");
141
+
142
+ let recovered_rev2 = store2.get(&id2).expect("get rev2");
143
+ assert_eq!(recovered_rev2.parent_id.as_ref().map(|id| id.as_hex()), Some(id1.as_hex()), "rev2 parent should be rev1");
144
+
145
+ let recovered_rev1 = store2.get(&id1).expect("get rev1");
146
+ assert!(recovered_rev1.parent_id.is_none(), "rev1 should have no parent");
147
+ }
148
+
149
+ #[test]
150
+ fn test_persistent_store_multiple_commits_in_sequence() {
151
+ let temp_dir = TempDir::new().unwrap();
152
+ let temp_path = temp_dir.path().join("test.log");
153
+
154
+ let db = feltdb::FeltDb::open(&temp_path).expect("open db");
155
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
156
+
157
+ // Commit sequence
158
+ let rev1 = store.create(r#"{"step":1}"#.to_string(), "app".to_string()).expect("1");
159
+ let rev2 = store.commit(r#"{"step":2}"#.to_string(), &rev1, "app".to_string()).expect("2");
160
+ let rev3 = store.commit(r#"{"step":3}"#.to_string(), &rev2, "app".to_string()).expect("3");
161
+ let rev4 = store.commit(r#"{"step":4}"#.to_string(), &rev3, "app".to_string()).expect("4");
162
+ let rev5 = store.commit(r#"{"step":5}"#.to_string(), &rev4, "app".to_string()).expect("5");
163
+
164
+ let id1 = rev1.id.clone();
165
+ let id2 = rev2.id.clone();
166
+ let id3 = rev3.id.clone();
167
+ let id4 = rev4.id.clone();
168
+ let id5 = rev5.id.clone();
169
+
170
+ // All should exist
171
+ assert!(store.exists(&id1));
172
+ assert!(store.exists(&id2));
173
+ assert!(store.exists(&id3));
174
+ assert!(store.exists(&id4));
175
+ assert!(store.exists(&id5));
176
+ let id5_hex = id5.as_hex().to_string();
177
+ assert_eq!(store.current().map(|r| r.id.as_hex().to_string()), Some(id5_hex));
178
+ }
179
+
180
+ #[test]
181
+ fn test_persistence_with_corrupted_content() {
182
+ let temp_dir = TempDir::new().unwrap();
183
+ let temp_path = temp_dir.path().join("test.log");
184
+
185
+ // Create and persist
186
+ let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
187
+ let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
188
+ let rev1 = store1.create(
189
+ r#"{"complex":{"nested":{"data":[1,2,3],"unicode":"αβγ"}}}"#.to_string(),
190
+ "auth".to_string()
191
+ ).expect("create");
192
+ let id1 = rev1.id.clone();
193
+ drop(store1);
194
+
195
+ // Recover
196
+ let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
197
+ let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
198
+ let recovered = store2.get(&id1).expect("get");
199
+ assert_eq!(recovered.content, r#"{"complex":{"nested":{"data":[1,2,3],"unicode":"αβγ"}}}"#);
200
+ }
201
+
202
+ #[test]
203
+ fn test_concurrent_access_to_persistent_store() {
204
+ let temp_dir = TempDir::new().unwrap();
205
+ let temp_path = temp_dir.path().join("test.log");
206
+
207
+ let db = feltdb::FeltDb::open(&temp_path).expect("open db");
208
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
209
+ let rev1 = store.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
210
+ let id1 = rev1.id.clone();
211
+
212
+ // Clone the store for concurrent access
213
+ let store_clone1 = store.clone();
214
+ let store_clone2 = store.clone();
215
+
216
+ let id1_hex = id1.as_hex().to_string();
217
+
218
+ // Both can read
219
+ assert_eq!(store_clone1.current().map(|r| r.id.as_hex().to_string()), Some(id1_hex.clone()));
220
+ assert_eq!(store_clone2.current().map(|r| r.id.as_hex().to_string()), Some(id1_hex));
221
+
222
+ // Commit from one
223
+ let rev2 = store_clone1.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit");
224
+ let id2 = rev2.id.clone();
225
+
226
+ // Both should see the new revision
227
+ assert!(store.exists(&id2));
228
+ assert!(store_clone2.exists(&id2));
229
+ }
230
+
231
+ #[test]
232
+ fn test_persistent_store_recovery_after_multiple_crashes() {
233
+ let temp_dir = TempDir::new().unwrap();
234
+ let temp_path = temp_dir.path().join("test.log");
235
+
236
+ // Cycle 1: create initial state
237
+ {
238
+ let db = feltdb::FeltDb::open(&temp_path).expect("open db");
239
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
240
+ let rev1 = store.create(r#"{"cycle":1}"#.to_string(), "auth".to_string()).expect("create1");
241
+ let _rev2 = store.commit(r#"{"cycle":1.5}"#.to_string(), &rev1, "auth".to_string()).expect("commit1");
242
+ }
243
+
244
+ // Cycle 2: recover and add more
245
+ {
246
+ let db = feltdb::FeltDb::open(&temp_path).expect("open db");
247
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
248
+ let rev2 = store.current().expect("current");
249
+ let _rev3 = store.commit(r#"{"cycle":2}"#.to_string(), &rev2, "auth".to_string()).expect("commit2");
250
+ }
251
+
252
+ // Cycle 3: verify all history
253
+ {
254
+ let db = feltdb::FeltDb::open(&temp_path).expect("open db");
255
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
256
+ assert!(store.current().is_some(), "should have current state after 3 cycles");
257
+
258
+ // Walk the chain backwards - count how many nodes we can reach
259
+ let mut rev = store.current().expect("current");
260
+ let mut count = 1; // Start with 1 for the current node
261
+ while let Some(parent_id) = &rev.parent_id {
262
+ rev = store.get(parent_id).expect("get parent");
263
+ count += 1;
264
+ }
265
+
266
+ assert!(count >= 3, "should have chain of at least 3 revisions, got {}", count);
267
+ }
268
+ }
269
+ }
270
+
@@ -1,8 +1,20 @@
1
1
  use std::path::PathBuf;
2
2
  use std::{
3
+ collections::HashMap,
3
4
  sync::{atomic::AtomicU64, Arc},
4
5
  time::Instant,
5
6
  };
7
+ use serde_json::Value;
8
+
9
+ #[derive(Clone)]
10
+ pub struct BoundedQueryCursor {
11
+ pub query_hash: String,
12
+ pub principal_key_id: String,
13
+ pub namespace: String,
14
+ pub records: Arc<Vec<Value>>,
15
+ pub position: usize,
16
+ pub created_at: u64,
17
+ }
6
18
 
7
19
  use feltdb::{
8
20
  FeltDb,
@@ -74,4 +86,5 @@ pub struct AppState {
74
86
  pub workloads: Arc<std::sync::Mutex<WorkloadStore>>,
75
87
  pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
76
88
  pub readiness_probe: Arc<PathBuf>,
89
+ pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
77
90
  }