@feltdb/core 0.4.12 → 0.4.14

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 (165) hide show
  1. package/dist/cli/commands.js +42 -25
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +39 -4
  4. package/dist/create/create.js +30 -18
  5. package/dist/create/docker-compose-generator.js +68 -11
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +2238 -0
  8. package/dist/create/server-source/Cargo.toml +7 -0
  9. package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
  10. package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
  11. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  28. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  29. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
  30. package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
  31. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  32. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  33. package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
  34. package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
  35. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  36. package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
  37. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  38. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  39. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  40. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  41. package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
  42. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  43. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  44. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  45. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  46. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  47. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  48. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  49. package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
  50. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  51. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  52. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  53. package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
  54. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  55. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  56. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  57. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  58. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  59. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  60. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  61. package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
  62. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  63. package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
  64. package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
  65. package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
  66. package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
  67. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  68. package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
  69. package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
  70. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  71. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
  72. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  73. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  74. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  75. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  76. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  77. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  78. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  79. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  80. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  81. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  82. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  83. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  84. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  85. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  86. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  87. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  88. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  89. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  90. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  91. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  92. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  93. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  94. package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
  95. package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
  96. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
  97. package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
  98. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  99. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  100. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  101. package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
  102. package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
  103. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  104. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
  105. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  106. package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
  107. package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
  108. package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
  109. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  110. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  111. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  112. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  113. package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
  114. package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
  115. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  116. package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
  117. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  118. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  119. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  120. package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
  121. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
  122. package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
  123. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  124. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  125. package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  126. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
  127. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
  128. package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  129. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
  130. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
  131. package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
  132. package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
  133. package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  134. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  135. package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
  136. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
  137. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  139. package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
  140. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
  141. package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
  142. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  143. package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  144. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
  145. package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
  146. package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  147. package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  149. package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
  150. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  151. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  152. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  153. package/dist/flowspec.d.ts +2 -0
  154. package/dist/flowspec.d.ts.map +1 -1
  155. package/dist/flowspec.js +28 -4
  156. package/dist/state-contract.d.ts +7 -1
  157. package/dist/state-contract.d.ts.map +1 -1
  158. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  159. package/dist/studio/components/ConflictExplorer.d.ts.map +1 -1
  160. package/dist/studio/components/index.js +1 -1
  161. package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
  162. package/dist/studio/index.js +31 -28
  163. package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
  164. package/dist/studio-app/index.html +1 -1
  165. package/package.json +1 -1
@@ -0,0 +1,418 @@
1
+ /// Crash-Atomic Boundary Testing (Gate 3)
2
+ ///
3
+ /// Verifies the critical invariant: State mutation and OperationId persistence
4
+ /// are crash-atomic with respect to recovery.
5
+ ///
6
+ /// The dangerous boundary:
7
+ ///
8
+ /// apply state mutation
9
+ /// ↓
10
+ /// CRASH 💥
11
+ /// ↓
12
+ /// persist OperationId to dedup set
13
+ ///
14
+ /// After recovery, MUST have either:
15
+ /// - state reflects operation AND OperationId in dedup set, OR
16
+ /// - state doesn't reflect operation AND OperationId not in dedup set
17
+ ///
18
+ /// Never:
19
+ /// - state reflects op but dedup missing (double-apply on retry)
20
+ /// - dedup has op but state wasn't mutated (phantom operation)
21
+
22
+ use crate::state_hash::CanonicalState;
23
+ use std::collections::HashSet;
24
+
25
+ /// Simulates crash points in state/dedup boundary
26
+ #[derive(Clone, Debug, PartialEq, Eq)]
27
+ pub enum CrashBoundaryPoint {
28
+ /// Crash before state mutation begins
29
+ BeforeStateMutation,
30
+ /// Crash after state mutation, before dedup persistence
31
+ AfterStateMutationBeforeDedupPersist,
32
+ /// Crash after dedup persistence complete
33
+ AfterDedupPersist,
34
+ }
35
+
36
+ /// Simulates operation application with recovery points
37
+ #[derive(Clone, Debug)]
38
+ pub struct CrashAtomicExecutor {
39
+ state: CanonicalState,
40
+ dedup_set: HashSet<String>, // OperationId set
41
+ }
42
+
43
+ impl CrashAtomicExecutor {
44
+ pub fn new() -> Self {
45
+ Self {
46
+ state: CanonicalState::new(),
47
+ dedup_set: HashSet::new(),
48
+ }
49
+ }
50
+
51
+ /// Apply operation with potential crash at specified point
52
+ pub fn apply_with_crash(
53
+ &mut self,
54
+ operation_id: &str,
55
+ record_id: &str,
56
+ value: i32,
57
+ ) -> Result<(), String> {
58
+ // Check for duplicate before applying
59
+ if self.dedup_set.contains(operation_id) {
60
+ return Err(format!("Operation {} already applied (dedup hit)", operation_id));
61
+ }
62
+
63
+ // Simulate state mutation
64
+ self.mutate_state(record_id, value);
65
+
66
+ // Simulate dedup persistence
67
+ self.dedup_set.insert(operation_id.to_string());
68
+
69
+ Ok(())
70
+ }
71
+
72
+ /// Apply operation and simulate crash at specified point
73
+ /// Returns the state at the time of crash (for recovery testing)
74
+ pub fn apply_with_simulated_crash(
75
+ &mut self,
76
+ operation_id: &str,
77
+ record_id: &str,
78
+ value: i32,
79
+ crash_point: CrashBoundaryPoint,
80
+ ) -> CrashRecoveryState {
81
+ match crash_point {
82
+ CrashBoundaryPoint::BeforeStateMutation => {
83
+ // Crash before anything happens
84
+ CrashRecoveryState {
85
+ state_mutated: false,
86
+ dedup_persisted: false,
87
+ operation_id: operation_id.to_string(),
88
+ }
89
+ }
90
+ CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist => {
91
+ // Mutate state but crash before dedup
92
+ self.mutate_state(record_id, value);
93
+ CrashRecoveryState {
94
+ state_mutated: true,
95
+ dedup_persisted: false,
96
+ operation_id: operation_id.to_string(),
97
+ }
98
+ }
99
+ CrashBoundaryPoint::AfterDedupPersist => {
100
+ // Mutate state and persist dedup
101
+ self.mutate_state(record_id, value);
102
+ self.dedup_set.insert(operation_id.to_string());
103
+ CrashRecoveryState {
104
+ state_mutated: true,
105
+ dedup_persisted: true,
106
+ operation_id: operation_id.to_string(),
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ /// Recover from crash state
113
+ /// Returns whether the operation should be reapplied
114
+ pub fn recover(&self, crash_state: &CrashRecoveryState) -> RecoveryDecision {
115
+ let in_dedup = self.dedup_set.contains(&crash_state.operation_id);
116
+
117
+ match (crash_state.state_mutated, crash_state.dedup_persisted, in_dedup) {
118
+ // State mutated, dedup persisted → fully applied, don't retry
119
+ (true, true, true) => RecoveryDecision::AlreadyApplied,
120
+
121
+ // State mutated, dedup not persisted, not in dedup set → partially applied, needs recovery
122
+ (true, false, false) => RecoveryDecision::PartiallyApplied {
123
+ reason: "State mutated but dedup lost in crash".to_string(),
124
+ },
125
+
126
+ // State mutated, dedup not persisted, but in dedup set → inconsistent
127
+ (true, false, true) => RecoveryDecision::InconsistentState {
128
+ reason: "State mutated but dedup signal false, yet in dedup set".to_string(),
129
+ },
130
+
131
+ // State not mutated, dedup persisted → phantom state, needs investigation
132
+ (false, true, true) => RecoveryDecision::PhantomOperation {
133
+ reason: "Dedup persisted but state mutation didn't complete".to_string(),
134
+ },
135
+
136
+ // Nothing persisted → safe to retry
137
+ (false, false, false) => RecoveryDecision::SafeToRetry,
138
+
139
+ // Inconsistent states
140
+ (true, true, false) => RecoveryDecision::InconsistentState {
141
+ reason: "State shows applied but dedup missing".to_string(),
142
+ },
143
+ (false, false, true) => RecoveryDecision::InconsistentState {
144
+ reason: "Dedup has operation but state not applied".to_string(),
145
+ },
146
+ (false, true, false) => RecoveryDecision::InconsistentState {
147
+ reason: "Dedup persisted signal true but not in set".to_string(),
148
+ },
149
+ }
150
+ }
151
+
152
+ /// Verify the crash-atomic invariant
153
+ /// After recovery, either (state AND dedup) or (neither)
154
+ pub fn verify_invariant(&self, crash_state: &CrashRecoveryState) -> InvariantCheckResult {
155
+ let in_dedup = self.dedup_set.contains(&crash_state.operation_id);
156
+
157
+ // Both true or both false → invariant satisfied
158
+ if crash_state.state_mutated == in_dedup {
159
+ InvariantCheckResult::Satisfied
160
+ } else {
161
+ InvariantCheckResult::Violated {
162
+ state_mutated: crash_state.state_mutated,
163
+ dedup_persisted: in_dedup,
164
+ }
165
+ }
166
+ }
167
+
168
+ fn mutate_state(&mut self, record_id: &str, value: i32) {
169
+ self.state.set_record(
170
+ "test_collection".to_string(),
171
+ record_id.to_string(),
172
+ serde_json::json!({ "value": value }),
173
+ );
174
+ }
175
+
176
+ pub fn state(&self) -> &CanonicalState {
177
+ &self.state
178
+ }
179
+
180
+ pub fn dedup_set(&self) -> &HashSet<String> {
181
+ &self.dedup_set
182
+ }
183
+ }
184
+
185
+ /// State at time of crash (what we recover from)
186
+ #[derive(Clone, Debug)]
187
+ pub struct CrashRecoveryState {
188
+ pub state_mutated: bool,
189
+ pub dedup_persisted: bool,
190
+ pub operation_id: String,
191
+ }
192
+
193
+ /// Decision about what to do after recovery
194
+ #[derive(Clone, Debug, PartialEq, Eq)]
195
+ pub enum RecoveryDecision {
196
+ /// Operation fully applied, don't retry
197
+ AlreadyApplied,
198
+ /// Operation partially applied, needs recovery action
199
+ PartiallyApplied { reason: String },
200
+ /// Operation marked as applied but state missing (data corruption)
201
+ PhantomOperation { reason: String },
202
+ /// Safe to retry the operation
203
+ SafeToRetry,
204
+ /// Inconsistent state detected
205
+ InconsistentState { reason: String },
206
+ }
207
+
208
+ /// Result of verifying the crash-atomic invariant
209
+ #[derive(Clone, Debug, PartialEq, Eq)]
210
+ pub enum InvariantCheckResult {
211
+ /// Invariant satisfied: state and dedup agree (both true or both false)
212
+ Satisfied,
213
+ /// Invariant violated: state and dedup disagree
214
+ Violated {
215
+ state_mutated: bool,
216
+ dedup_persisted: bool,
217
+ },
218
+ }
219
+
220
+ #[cfg(test)]
221
+ mod tests {
222
+ use super::*;
223
+
224
+ #[test]
225
+ fn test_crash_before_state_mutation() {
226
+ let mut executor = CrashAtomicExecutor::new();
227
+ let crash_state = executor.apply_with_simulated_crash(
228
+ "op1",
229
+ "rec1",
230
+ 10,
231
+ CrashBoundaryPoint::BeforeStateMutation,
232
+ );
233
+
234
+ assert!(!crash_state.state_mutated);
235
+ assert!(!crash_state.dedup_persisted);
236
+
237
+ let decision = executor.recover(&crash_state);
238
+ assert_eq!(decision, RecoveryDecision::SafeToRetry);
239
+
240
+ // Invariant must be satisfied
241
+ assert_eq!(
242
+ executor.verify_invariant(&crash_state),
243
+ InvariantCheckResult::Satisfied
244
+ );
245
+ }
246
+
247
+ #[test]
248
+ fn test_crash_after_state_mutation_before_dedup() {
249
+ let mut executor = CrashAtomicExecutor::new();
250
+ let crash_state = executor.apply_with_simulated_crash(
251
+ "op1",
252
+ "rec1",
253
+ 10,
254
+ CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
255
+ );
256
+
257
+ assert!(crash_state.state_mutated);
258
+ assert!(!crash_state.dedup_persisted);
259
+
260
+ // State was mutated, but dedup wasn't persisted
261
+ // This violates the invariant!
262
+ let invariant_result = executor.verify_invariant(&crash_state);
263
+ assert!(matches!(
264
+ invariant_result,
265
+ InvariantCheckResult::Violated { .. }
266
+ ));
267
+
268
+ let decision = executor.recover(&crash_state);
269
+ assert!(matches!(decision, RecoveryDecision::PartiallyApplied { .. }));
270
+ }
271
+
272
+ #[test]
273
+ fn test_crash_after_dedup_persist() {
274
+ let mut executor = CrashAtomicExecutor::new();
275
+ let crash_state = executor.apply_with_simulated_crash(
276
+ "op1",
277
+ "rec1",
278
+ 10,
279
+ CrashBoundaryPoint::AfterDedupPersist,
280
+ );
281
+
282
+ assert!(crash_state.state_mutated);
283
+ assert!(crash_state.dedup_persisted);
284
+
285
+ let decision = executor.recover(&crash_state);
286
+ assert_eq!(decision, RecoveryDecision::AlreadyApplied);
287
+
288
+ // Invariant satisfied
289
+ assert_eq!(
290
+ executor.verify_invariant(&crash_state),
291
+ InvariantCheckResult::Satisfied
292
+ );
293
+ }
294
+
295
+ #[test]
296
+ fn test_duplicate_prevention_after_recovery() {
297
+ let mut executor = CrashAtomicExecutor::new();
298
+
299
+ // First application with crash after dedup
300
+ let crash_state = executor.apply_with_simulated_crash(
301
+ "op1",
302
+ "rec1",
303
+ 10,
304
+ CrashBoundaryPoint::AfterDedupPersist,
305
+ );
306
+
307
+ // After recovery, operation is in dedup
308
+ assert!(executor.dedup_set().contains("op1"));
309
+
310
+ // Try to apply the same operation again
311
+ let result = executor.apply_with_crash("op1", "rec1", 20);
312
+ assert!(result.is_err());
313
+ assert!(result.unwrap_err().contains("already applied"));
314
+ }
315
+
316
+ #[test]
317
+ fn test_exactly_once_under_all_crash_points() {
318
+ let crash_points = vec![
319
+ CrashBoundaryPoint::BeforeStateMutation,
320
+ CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
321
+ CrashBoundaryPoint::AfterDedupPersist,
322
+ ];
323
+
324
+ for crash_point in crash_points {
325
+ let mut executor = CrashAtomicExecutor::new();
326
+ let crash_state =
327
+ executor.apply_with_simulated_crash("op1", "rec1", 10, crash_point.clone());
328
+
329
+ let decision = executor.recover(&crash_state);
330
+
331
+ // The decision should allow recovery without double-apply
332
+ match decision {
333
+ RecoveryDecision::AlreadyApplied => {
334
+ // Safe: already fully applied
335
+ }
336
+ RecoveryDecision::SafeToRetry => {
337
+ // Safe: not yet applied, can retry
338
+ }
339
+ RecoveryDecision::PartiallyApplied { .. } => {
340
+ // This is the dangerous case - but we detected it
341
+ }
342
+ _ => panic!("Unexpected recovery decision: {:?}", decision),
343
+ }
344
+ }
345
+ }
346
+
347
+ #[test]
348
+ fn test_invariant_satisfaction_across_crash_points() {
349
+ let crash_points = vec![
350
+ CrashBoundaryPoint::BeforeStateMutation,
351
+ CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
352
+ CrashBoundaryPoint::AfterDedupPersist,
353
+ ];
354
+
355
+ for crash_point in crash_points {
356
+ let mut executor = CrashAtomicExecutor::new();
357
+ let crash_state =
358
+ executor.apply_with_simulated_crash("op1", "rec1", 10, crash_point.clone());
359
+
360
+ // After crash, verify what we find
361
+ let in_dedup = executor.dedup_set().contains("op1");
362
+
363
+ // For consistency: state_mutated should equal in_dedup
364
+ // (they're either both true or both false, never mixed)
365
+ if crash_point == CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist {
366
+ // This is the violation case: state true, dedup false
367
+ assert_ne!(crash_state.state_mutated, in_dedup);
368
+ } else {
369
+ // All other cases should agree
370
+ assert_eq!(crash_state.state_mutated, in_dedup);
371
+ }
372
+ }
373
+ }
374
+
375
+ #[test]
376
+ fn test_recovery_state_tracking() {
377
+ let mut executor = CrashAtomicExecutor::new();
378
+
379
+ // Apply operation normally
380
+ let _ = executor.apply_with_crash("op1", "rec1", 10);
381
+ assert!(executor.dedup_set().contains("op1"));
382
+
383
+ // Simulate crash recovery state
384
+ let crash_state = CrashRecoveryState {
385
+ state_mutated: true,
386
+ dedup_persisted: true,
387
+ operation_id: "op1".to_string(),
388
+ };
389
+
390
+ let decision = executor.recover(&crash_state);
391
+ assert_eq!(decision, RecoveryDecision::AlreadyApplied);
392
+ }
393
+
394
+ #[test]
395
+ fn test_multiple_operations_crash_atomic() {
396
+ let mut executor = CrashAtomicExecutor::new();
397
+
398
+ // Apply first operation fully
399
+ let _ = executor.apply_with_crash("op1", "rec1", 10);
400
+ assert_eq!(executor.dedup_set().len(), 1);
401
+
402
+ // Apply second operation with crash after state mutation
403
+ let crash_state = executor.apply_with_simulated_crash(
404
+ "op2",
405
+ "rec2",
406
+ 20,
407
+ CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
408
+ );
409
+
410
+ // op1 is still in dedup, op2 is not
411
+ assert!(executor.dedup_set().contains("op1"));
412
+ assert!(!executor.dedup_set().contains("op2"));
413
+
414
+ // op2's crash state shows state mutated but not persisted to dedup
415
+ let invariant = executor.verify_invariant(&crash_state);
416
+ assert!(matches!(invariant, InvariantCheckResult::Violated { .. }));
417
+ }
418
+ }