@feltdb/core 0.6.9 → 0.6.10

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 (98) hide show
  1. package/dist/cli/commands.js +189 -71
  2. package/dist/cli/index.js +1 -1
  3. package/dist/collection.d.ts +110 -2
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +228 -3
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
  8. package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
  9. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
  10. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
  11. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
  12. package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
  13. package/dist/db.d.ts +59 -0
  14. package/dist/db.d.ts.map +1 -1
  15. package/dist/db.js +94 -2
  16. package/dist/embedded-transaction.d.ts +77 -0
  17. package/dist/embedded-transaction.d.ts.map +1 -0
  18. package/dist/embedded-transaction.js +102 -0
  19. package/dist/feltdb.d.ts +49 -1
  20. package/dist/feltdb.d.ts.map +1 -1
  21. package/dist/file-db.d.ts +33 -0
  22. package/dist/file-db.d.ts.map +1 -1
  23. package/dist/file-db.js +128 -4
  24. package/dist/freshness.d.ts +119 -0
  25. package/dist/freshness.d.ts.map +1 -0
  26. package/dist/freshness.js +115 -0
  27. package/dist/http-db.d.ts +48 -0
  28. package/dist/http-db.d.ts.map +1 -1
  29. package/dist/http-db.js +77 -1
  30. package/dist/index-backend.d.ts +8 -0
  31. package/dist/index-backend.d.ts.map +1 -1
  32. package/dist/index-backend.js +13 -0
  33. package/dist/index-core.d.ts +78 -0
  34. package/dist/index-core.d.ts.map +1 -0
  35. package/dist/index-core.js +72 -0
  36. package/dist/index-manager.d.ts +11 -0
  37. package/dist/index-manager.d.ts.map +1 -1
  38. package/dist/index-manager.js +30 -13
  39. package/dist/index-types.d.ts +17 -0
  40. package/dist/index-types.d.ts.map +1 -1
  41. package/dist/index.browser.d.ts +14 -0
  42. package/dist/index.browser.d.ts.map +1 -0
  43. package/dist/index.browser.js +13 -0
  44. package/dist/index.d.ts +8 -59
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +11 -54
  47. package/dist/indexeddb-db.d.ts +32 -0
  48. package/dist/indexeddb-db.d.ts.map +1 -1
  49. package/dist/indexeddb-db.js +151 -1
  50. package/dist/memory-db.d.ts +26 -0
  51. package/dist/memory-db.d.ts.map +1 -1
  52. package/dist/memory-db.js +86 -0
  53. package/dist/runtime-registry.d.ts +32 -0
  54. package/dist/runtime-registry.d.ts.map +1 -0
  55. package/dist/runtime-registry.js +38 -0
  56. package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
  57. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  58. package/dist/studio-app/assets/index-D3rT0SJi.js +28 -0
  59. package/dist/studio-app/index.html +1 -1
  60. package/dist/transaction.d.ts +98 -0
  61. package/dist/transaction.d.ts.map +1 -0
  62. package/dist/transaction.js +86 -0
  63. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  64. package/dist/workspace/browser.d.ts +22 -0
  65. package/dist/workspace/browser.d.ts.map +1 -0
  66. package/dist/workspace/browser.js +17 -0
  67. package/dist/workspace/git-identity.d.ts +35 -0
  68. package/dist/workspace/git-identity.d.ts.map +1 -0
  69. package/dist/workspace/git-identity.js +81 -0
  70. package/dist/workspace/index.d.ts +11 -1
  71. package/dist/workspace/index.d.ts.map +1 -1
  72. package/dist/workspace/index.js +6 -0
  73. package/dist/workspace/investigation-analysis.d.ts +47 -0
  74. package/dist/workspace/investigation-analysis.d.ts.map +1 -0
  75. package/dist/workspace/investigation-analysis.js +181 -0
  76. package/dist/workspace/investigation-lifecycle-manager.d.ts +86 -0
  77. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
  78. package/dist/workspace/investigation-lifecycle-manager.js +229 -0
  79. package/dist/workspace/investigation-supervisor.d.ts +100 -0
  80. package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
  81. package/dist/workspace/investigation-supervisor.js +236 -0
  82. package/dist/workspace/runtime-investigation.d.ts +1 -1
  83. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  84. package/dist/workspace/runtime-investigation.js +46 -21
  85. package/dist/workspace/runtime-observation.d.ts +79 -0
  86. package/dist/workspace/runtime-observation.d.ts.map +1 -0
  87. package/dist/workspace/runtime-observation.js +152 -0
  88. package/dist/workspace/runtime-observer.d.ts +51 -0
  89. package/dist/workspace/runtime-observer.d.ts.map +1 -0
  90. package/dist/workspace/runtime-observer.js +268 -0
  91. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  92. package/dist/workspace/workspace-connection.js +14 -5
  93. package/dist/workspace/workspace-types.d.ts +113 -0
  94. package/dist/workspace/workspace-types.d.ts.map +1 -1
  95. package/package.json +12 -4
  96. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
  97. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  98. package/dist/studio-app/assets/index-BF45M757.js +0 -28
@@ -1,523 +0,0 @@
1
- /// Phase 1c: Three-Node TCP Scaling
2
- ///
3
- /// Validates that FeltDB replication converges correctly with 3 nodes over real TCP.
4
- ///
5
- /// Success Criteria:
6
- /// - 3-node cluster established over TCP (1 leader + 2 followers)
7
- /// - 1000 operations replicated to all 3 nodes
8
- /// - All 3 nodes converge to identical state_hash
9
- /// - Replication latency <1s (p99) on LAN
10
- /// - Zero message loss or corruption
11
- /// - Test passes 10 consecutive runs
12
- /// - No deadlocks or stalls
13
-
14
- #[cfg(test)]
15
- mod tests {
16
- use crate::convergence::VectorClock;
17
- use crate::distributed_transactions::{
18
- DistributedTransactionExecutor, ReplicationMessage, TransactionEnvelope,
19
- };
20
- use crate::in_process_transport::InProcessTransport;
21
- use crate::replication_protocol::ProtocolTransport;
22
- use crate::state_hash::StateHash;
23
- use crate::tcp_transport::TcpTransport;
24
- use crate::transactions::{ConsistencyContract, Operation, OperationCommand, OperationId, StateVersion};
25
- use serde_json::json;
26
- use std::collections::HashMap;
27
- use std::sync::Arc;
28
- use std::time::{Duration, Instant};
29
- use tokio::sync::Mutex;
30
- use tokio::task::{self, JoinSet};
31
-
32
- /// Test node that combines FeltDb state with TCP transport
33
- struct TestNode {
34
- node_id: String,
35
- port: u16,
36
- executor: Arc<Mutex<DistributedTransactionExecutor>>,
37
- transport: Arc<Mutex<TcpTransport>>,
38
- sequence: Arc<Mutex<u64>>,
39
- }
40
-
41
- impl TestNode {
42
- fn new(node_id: String, port: u16) -> Self {
43
- let initial_hash = StateHash::from_hex("0".repeat(64));
44
- let executor = DistributedTransactionExecutor::new(node_id.clone(), initial_hash);
45
-
46
- let addr = format!("127.0.0.1:{}", port);
47
- let transport = TcpTransport::new(addr);
48
-
49
- Self {
50
- node_id,
51
- port,
52
- executor: Arc::new(Mutex::new(executor)),
53
- transport: Arc::new(Mutex::new(transport)),
54
- sequence: Arc::new(Mutex::new(0)),
55
- }
56
- }
57
-
58
- /// Get the bind address for this node
59
- fn addr(&self) -> String {
60
- format!("127.0.0.1:{}", self.port)
61
- }
62
-
63
- /// Get current state hash
64
- async fn state_hash(&self) -> StateHash {
65
- let executor = self.executor.lock().await;
66
- executor
67
- .get_replica_state(&self.node_id)
68
- .map(|s| s.state_hash)
69
- .unwrap_or_else(|| StateHash::from_hex("0".repeat(64)))
70
- }
71
-
72
- /// Check if this node has received all expected envelopes
73
- async fn operations_applied(&self) -> u64 {
74
- let executor = self.executor.lock().await;
75
- executor
76
- .get_replica_state(&self.node_id)
77
- .map(|s| s.operations_applied)
78
- .unwrap_or(0)
79
- }
80
-
81
- /// Submit a single operation (leader only)
82
- async fn submit_operation(&self, op_idx: u64) -> Result<(), String> {
83
- let mut executor = self.executor.lock().await;
84
- let mut seq = self.sequence.lock().await;
85
- *seq += 1;
86
-
87
- let op_id = OperationId::new(self.node_id.clone(), *seq);
88
- let parent_vc = VectorClock::new();
89
- let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
90
-
91
- let mut fields = HashMap::new();
92
- fields.insert("value".to_string(), json!(op_idx));
93
-
94
- let operation = Operation::new(
95
- op_id,
96
- parent_version.clone(),
97
- format!("op_{}", op_idx),
98
- OperationCommand {
99
- op_type: "set".to_string(),
100
- collection: "items".to_string(),
101
- record_id: format!("item_{}", op_idx),
102
- fields,
103
- },
104
- self.node_id.clone(),
105
- );
106
-
107
- let envelope = executor.execute_local_transaction(
108
- *seq,
109
- format!("txn_{}", op_idx),
110
- parent_version,
111
- vec![operation],
112
- ConsistencyContract::causal(),
113
- )?;
114
-
115
- Ok(())
116
- }
117
- }
118
-
119
- /// Test coordinator for managing 3-node cluster
120
- struct TestCluster {
121
- nodes: Vec<TestNode>,
122
- }
123
-
124
- impl TestCluster {
125
- fn new(node_count: usize) -> Self {
126
- let mut nodes = Vec::new();
127
- for i in 0..node_count {
128
- let node_id = format!("node-{}", i);
129
- let port = 10000 + i as u16;
130
- nodes.push(TestNode::new(node_id, port));
131
- }
132
- Self { nodes }
133
- }
134
-
135
- /// Get node by index
136
- fn node(&self, idx: usize) -> &TestNode {
137
- &self.nodes[idx]
138
- }
139
-
140
- /// Start listening on all nodes
141
- async fn start_listeners(&self) {
142
- for node in &self.nodes {
143
- let transport = node.transport.clone();
144
- let _handle = task::spawn(async move {
145
- let mut t = transport.lock().await;
146
- let _ = t.listen().await;
147
- });
148
- }
149
- // Give listeners time to start
150
- tokio::time::sleep(Duration::from_millis(100)).await;
151
- }
152
-
153
- /// Connect followers to leader
154
- async fn establish_replication_topology(&self) {
155
- let leader_addr = self.nodes[0].addr();
156
-
157
- for node in self.nodes.iter().skip(1) {
158
- let mut t = node.transport.lock().await;
159
- let _ = (*t).connect(&leader_addr).await;
160
- }
161
- }
162
-
163
- /// Simulate replication: broadcast leader messages to all followers
164
- async fn replicate_from_leader(&self, operations_count: u64) {
165
- let leader = &self.nodes[0];
166
- let followers: Vec<_> = (1..self.nodes.len()).collect();
167
-
168
- // Wait for operations to be applied on followers
169
- for op_idx in 0..operations_count {
170
- // Small delay between operations to allow replication
171
- tokio::time::sleep(Duration::from_millis(1)).await;
172
- }
173
- }
174
-
175
- /// Verify all nodes have converged to same state
176
- async fn verify_convergence(&self) -> Result<(), String> {
177
- let mut hashes = Vec::new();
178
-
179
- for node in &self.nodes {
180
- let executor = node.executor.lock().await;
181
- let state = executor.get_replica_state(&node.node_id);
182
- if let Some(s) = state {
183
- hashes.push(s.state_hash);
184
- } else {
185
- return Err(format!("Node {} has no state", node.node_id));
186
- }
187
- }
188
-
189
- if hashes.is_empty() {
190
- return Err("No nodes".to_string());
191
- }
192
-
193
- let first = &hashes[0];
194
- for (idx, hash) in hashes.iter().enumerate().skip(1) {
195
- if hash != first {
196
- return Err(format!(
197
- "Convergence failed: node-0 has {}, node-{} has {}",
198
- first, idx, hash
199
- ));
200
- }
201
- }
202
-
203
- Ok(())
204
- }
205
-
206
- /// Get statistics about replication
207
- async fn get_stats(&self) -> ClusterStats {
208
- let mut total_ops_applied = 0;
209
- let mut node_states = Vec::new();
210
-
211
- for node in &self.nodes {
212
- let ops = node.operations_applied().await;
213
- let hash = node.state_hash().await;
214
- total_ops_applied += ops;
215
- node_states.push((node.node_id.clone(), ops, hash));
216
- }
217
-
218
- ClusterStats {
219
- node_states,
220
- total_ops_applied,
221
- }
222
- }
223
- }
224
-
225
- struct ClusterStats {
226
- node_states: Vec<(String, u64, StateHash)>,
227
- total_ops_applied: u64,
228
- }
229
-
230
- /// Phase 1c Test 1: Cluster initialization (no networking yet)
231
- #[tokio::test]
232
- async fn three_node_cluster_initializes() {
233
- let cluster = TestCluster::new(3);
234
-
235
- // Verify all nodes are created with initialized state
236
- for node in &cluster.nodes {
237
- let hash = node.state_hash().await;
238
- // Hash should be deterministic initial value
239
- assert_ne!(hash, StateHash::from_hex("".to_string()), "Node {} should have state", node.node_id);
240
- }
241
- }
242
-
243
- /// Phase 1c Test 2: Submit single operation to leader
244
- #[tokio::test]
245
- async fn leader_submit_single_operation() {
246
- let cluster = TestCluster::new(3);
247
- let leader = cluster.node(0);
248
-
249
- // Submit first operation
250
- leader
251
- .submit_operation(0)
252
- .await
253
- .expect("submit should succeed");
254
-
255
- // Verify leader state changed
256
- let ops_applied = leader.operations_applied().await;
257
- assert_eq!(ops_applied, 1, "Leader should have 1 operation applied");
258
- }
259
-
260
- /// Phase 1c Test 3: Submit 100 operations to leader
261
- #[tokio::test]
262
- async fn leader_submit_100_operations() {
263
- let cluster = TestCluster::new(3);
264
- let leader = cluster.node(0);
265
- let start = Instant::now();
266
-
267
- // Submit 100 operations through leader
268
- for i in 0..100 {
269
- leader
270
- .submit_operation(i)
271
- .await
272
- .expect("submit should succeed");
273
- }
274
-
275
- // Check leader has all operations
276
- let leader_ops = leader.operations_applied().await;
277
- assert_eq!(leader_ops, 100, "Leader should have 100 operations applied");
278
-
279
- println!(
280
- "Submitted 100 operations in {:?}",
281
- start.elapsed()
282
- );
283
- }
284
-
285
- /// Phase 1c Test 4: Leader can track multiple operations
286
- #[tokio::test]
287
- async fn leader_tracks_operation_sequence() {
288
- let cluster = TestCluster::new(3);
289
- let leader = cluster.node(0);
290
-
291
- // Submit multiple operations
292
- for i in 0..50 {
293
- leader
294
- .submit_operation(i)
295
- .await
296
- .expect("submit should succeed");
297
- }
298
-
299
- let ops_applied = leader.operations_applied().await;
300
- assert_eq!(ops_applied, 50, "Leader should track all 50 operations");
301
- }
302
-
303
- /// Phase 1c Test 5: Submit 1000 operations to leader
304
- #[tokio::test]
305
- async fn leader_submit_1000_operations() {
306
- let cluster = TestCluster::new(3);
307
- let leader = cluster.node(0);
308
- let start = Instant::now();
309
-
310
- // Submit 1000 operations
311
- for i in 0..1000 {
312
- leader
313
- .submit_operation(i)
314
- .await
315
- .expect("submit should succeed");
316
-
317
- // Add small delay to avoid overwhelming the system
318
- if i % 100 == 0 {
319
- tokio::time::sleep(Duration::from_millis(1)).await;
320
- }
321
- }
322
-
323
- let elapsed = start.elapsed();
324
- let leader_ops = leader.operations_applied().await;
325
-
326
- println!("Submitted 1000 operations in {:?}", elapsed);
327
- println!("Leader has {} operations applied", leader_ops);
328
-
329
- assert_eq!(leader_ops, 1000, "Leader should have all 1000 operations applied");
330
- }
331
-
332
- /// Phase 1c Test 6: Multiple nodes can submit operations independently
333
- #[tokio::test]
334
- async fn multiple_nodes_submit_independently() {
335
- let cluster = TestCluster::new(3);
336
- let node0 = cluster.node(0);
337
- let node1 = cluster.node(1);
338
- let node2 = cluster.node(2);
339
-
340
- // Each node submits operations independently
341
- for i in 0..10 {
342
- node0.submit_operation(i).await.expect("node0 submit");
343
- node1.submit_operation(i + 1000).await.expect("node1 submit");
344
- node2.submit_operation(i + 2000).await.expect("node2 submit");
345
- }
346
-
347
- // Verify each node tracks its operations
348
- assert_eq!(node0.operations_applied().await, 10, "Node0 ops");
349
- assert_eq!(node1.operations_applied().await, 10, "Node1 ops");
350
- assert_eq!(node2.operations_applied().await, 10, "Node2 ops");
351
-
352
- println!("All 3 nodes submitted 10 operations each independently");
353
- }
354
-
355
- /// Replication Test 1: Single operation replication between two nodes
356
- #[tokio::test]
357
- async fn replicate_single_operation_to_follower() {
358
- let leader_id = "leader".to_string();
359
- let follower_id = "follower".to_string();
360
-
361
- let initial_hash = StateHash::from_hex("0".repeat(64));
362
- let mut leader_executor = DistributedTransactionExecutor::new(leader_id.clone(), initial_hash.clone());
363
- let mut follower_executor = DistributedTransactionExecutor::new(follower_id.clone(), initial_hash.clone());
364
-
365
- // Register replicas
366
- leader_executor.register_replica(follower_id.clone(), initial_hash.clone());
367
- follower_executor.register_replica(leader_id.clone(), initial_hash.clone());
368
-
369
- // Leader submits operation
370
- let op_id = OperationId::new(leader_id.clone(), 1);
371
- let parent_vc = VectorClock::new();
372
- let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
373
-
374
- let mut fields = HashMap::new();
375
- fields.insert("value".to_string(), json!(42));
376
-
377
- let operation = Operation::new(
378
- op_id,
379
- parent_version.clone(),
380
- "op_1".to_string(),
381
- OperationCommand {
382
- op_type: "set".to_string(),
383
- collection: "items".to_string(),
384
- record_id: "item_1".to_string(),
385
- fields,
386
- },
387
- leader_id.clone(),
388
- );
389
-
390
- let envelope = leader_executor
391
- .execute_local_transaction(
392
- 1,
393
- "txn_1".to_string(),
394
- parent_version.clone(),
395
- vec![operation],
396
- ConsistencyContract::causal(),
397
- )
398
- .expect("leader should execute");
399
-
400
- // Create replication message
401
- let message = ReplicationMessage::new(
402
- envelope,
403
- leader_id.clone(),
404
- follower_id.clone(),
405
- 1,
406
- );
407
-
408
- // Follower applies replicated operation
409
- let follower_result = follower_executor.receive_replicated_transaction(message, parent_version);
410
- assert!(follower_result.is_ok(), "Follower should apply operation");
411
-
412
- // Verify leader and follower have same state
413
- let leader_state = leader_executor.get_replica_state(&leader_id).unwrap();
414
- let follower_state = follower_executor.get_replica_state(&follower_id).unwrap();
415
-
416
- println!(
417
- "Leader hash: {}, Follower hash: {}",
418
- leader_state.state_hash, follower_state.state_hash
419
- );
420
- println!(
421
- "Leader ops: {}, Follower ops: {}",
422
- leader_state.operations_applied, follower_state.operations_applied
423
- );
424
- }
425
-
426
- /// Replication Test 2: Multiple operations converge across 3 nodes
427
- #[tokio::test]
428
- async fn three_node_convergence_with_replication() {
429
- let node_ids = ["leader", "follower1", "follower2"];
430
- let mut executors: Vec<_> = node_ids
431
- .iter()
432
- .map(|id| {
433
- let initial_hash = StateHash::from_hex("0".repeat(64));
434
- DistributedTransactionExecutor::new(id.to_string(), initial_hash)
435
- })
436
- .collect();
437
-
438
- let initial_hash = StateHash::from_hex("0".repeat(64));
439
-
440
- // Register replicas on all executors
441
- for executor in &mut executors {
442
- for node_id in &node_ids {
443
- if node_id != &executor.local_node_id.as_str() {
444
- executor.register_replica(node_id.to_string(), initial_hash.clone());
445
- }
446
- }
447
- }
448
-
449
- // Leader submits 10 operations
450
- let mut envelopes = Vec::new();
451
- for i in 0..10 {
452
- let op_id = OperationId::new("leader".to_string(), i + 1);
453
- let parent_vc = VectorClock::new();
454
- let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
455
-
456
- let mut fields = HashMap::new();
457
- fields.insert("value".to_string(), json!(i));
458
-
459
- let operation = Operation::new(
460
- op_id,
461
- parent_version.clone(),
462
- format!("op_{}", i),
463
- OperationCommand {
464
- op_type: "set".to_string(),
465
- collection: "items".to_string(),
466
- record_id: format!("item_{}", i),
467
- fields,
468
- },
469
- "leader".to_string(),
470
- );
471
-
472
- let envelope = executors[0]
473
- .execute_local_transaction(
474
- i + 1,
475
- format!("txn_{}", i),
476
- parent_version,
477
- vec![operation],
478
- ConsistencyContract::causal(),
479
- )
480
- .expect("leader execute");
481
-
482
- envelopes.push(envelope);
483
- }
484
-
485
- // Followers apply operations
486
- for (follower_idx, follower_id) in node_ids.iter().enumerate().skip(1) {
487
- for envelope in &envelopes {
488
- let message = ReplicationMessage::new(
489
- envelope.clone(),
490
- "leader".to_string(),
491
- follower_id.to_string(),
492
- 0,
493
- );
494
-
495
- let parent_version = StateVersion::new(VectorClock::new(), "0".repeat(64));
496
- let _ = executors[follower_idx].receive_replicated_transaction(message, parent_version);
497
- }
498
- }
499
-
500
- // Verify convergence
501
- let leader_state = executors[0].get_replica_state("leader").unwrap();
502
- let follower1_state = executors[1].get_replica_state("follower1").unwrap();
503
- let follower2_state = executors[2].get_replica_state("follower2").unwrap();
504
-
505
- println!(
506
- "Leader: {} ops, hash {}",
507
- leader_state.operations_applied, leader_state.state_hash
508
- );
509
- println!(
510
- "Follower1: {} ops, hash {}",
511
- follower1_state.operations_applied, follower1_state.state_hash
512
- );
513
- println!(
514
- "Follower2: {} ops, hash {}",
515
- follower2_state.operations_applied, follower2_state.state_hash
516
- );
517
-
518
- // All nodes should have applied the same number of operations on their respective views
519
- assert_eq!(leader_state.operations_applied, 10, "Leader ops");
520
- assert_eq!(follower1_state.operations_applied, 10, "Follower1 ops");
521
- assert_eq!(follower2_state.operations_applied, 10, "Follower2 ops");
522
- }
523
- }