@feltdb/core 0.8.4 → 0.8.5

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 (149) hide show
  1. package/dist/create/package-versions.js +1 -1
  2. package/dist/create/server-source/Cargo.lock +165 -0
  3. package/dist/create/server-source/Cargo.toml +9 -0
  4. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  5. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  6. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  7. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  8. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  9. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  10. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  11. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  12. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  13. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  14. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  15. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  19. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  20. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  21. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  22. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  23. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  24. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  25. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  26. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  27. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  28. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  29. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  30. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  31. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  32. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  33. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  34. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  35. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  36. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  37. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  38. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  39. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  40. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  41. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  42. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  43. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  44. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  45. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  46. package/dist/create/server-source/crates/feltdb/src/lib.rs +507 -6
  47. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  48. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  49. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  50. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  51. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  52. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  53. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  54. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  55. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  56. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  57. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  58. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  59. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  60. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  61. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  62. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
  63. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  64. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  65. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  66. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  67. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  68. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  69. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  70. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  71. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  72. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  73. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  74. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  75. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  76. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  77. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  78. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  79. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  80. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  81. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  82. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  83. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  84. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  85. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  86. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
  87. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  88. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  89. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  90. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
  91. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  92. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  93. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  94. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  95. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  96. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  97. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  98. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  99. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  100. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  101. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  102. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
  103. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  104. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
  105. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  106. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  107. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  108. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  109. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  110. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
  111. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
  112. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  113. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  114. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  115. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  116. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  117. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  118. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  119. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  120. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  121. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
  122. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  123. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  124. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  125. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  126. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  127. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  128. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  129. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  130. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  131. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  132. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  133. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  134. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  135. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  136. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  137. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
  139. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  140. package/dist/create/server-source/crates/feltdb-server/src/main.rs +326 -6
  141. package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-DaNwCLRX.js} +1 -1
  142. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
  143. package/dist/studio-app/assets/{index-DwgNAIIX.js → index-j8IlhNqJ.js} +1 -1
  144. package/dist/studio-app/index.html +1 -1
  145. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  146. package/dist/workload.d.ts +2 -1
  147. package/dist/workload.d.ts.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
@@ -0,0 +1,994 @@
1
+ //! Conformance, determinism and capability proofs for the
2
+ //! `feltdb.trigger.match` deterministic contract.
3
+ //!
4
+ //! The architectural invariant under test is:
5
+ //!
6
+ //! ```text
7
+ //! Contract semantics are authoritative.
8
+ //! Runtime implementation is replaceable.
9
+ //!
10
+ //! NativeContract(input) == WasmContract(input) for every fixture
11
+ //! ```
12
+ //!
13
+ //! The corpus in `tests/fixtures/trigger_contract_corpus.json` is committed
14
+ //! rather than generated, and carries the canonical output each case must
15
+ //! produce. Three comparisons run over it:
16
+ //!
17
+ //! 1. native output == committed expectation (runs everywhere);
18
+ //! 2. WASM output == committed expectation (needs `wasm32-unknown-unknown`);
19
+ //! 3. native output == WASM output, byte for byte.
20
+ //!
21
+ //! The first makes the suite meaningful even where the WASM toolchain is
22
+ //! absent: a change in native semantics is caught by the same bytes the WASM
23
+ //! implementation was measured against.
24
+
25
+ use feltdb::trigger_contract::{
26
+ canonical_input_bytes, evaluate_canonical, TriggerMatchInput, TRIGGER_MATCH_CONTRACT_ID,
27
+ TRIGGER_MATCH_CONTRACT_VERSION,
28
+ };
29
+ use serde::Deserialize;
30
+ use serde_json::Value;
31
+ use std::path::{Path, PathBuf};
32
+ use std::process::Command;
33
+ use std::sync::OnceLock;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Corpus
37
+ // ---------------------------------------------------------------------------
38
+
39
+ #[derive(Debug, Deserialize)]
40
+ struct Corpus {
41
+ corpus_version: u32,
42
+ contract: String,
43
+ contract_version: u32,
44
+ #[allow(dead_code)]
45
+ about: String,
46
+ cases: Vec<Case>,
47
+ }
48
+
49
+ #[derive(Debug, Deserialize)]
50
+ struct Case {
51
+ name: String,
52
+ #[allow(dead_code)]
53
+ why: String,
54
+ /// A well-formed contract input, canonically encoded by the harness.
55
+ #[serde(default)]
56
+ input: Option<TriggerMatchInput>,
57
+ /// Bytes fed to the contract verbatim, for malformed and non-canonical
58
+ /// encodings that a typed value cannot express.
59
+ #[serde(default)]
60
+ raw_input: Option<String>,
61
+ /// The canonical encoding of `input`, recorded so that every consumer of
62
+ /// the corpus presents the same bytes.
63
+ ///
64
+ /// A consumer in another language cannot always reproduce it: JavaScript
65
+ /// has one number type, so it cannot encode the difference between `42` and
66
+ /// `42.0` that this contract's value equality depends on. Recording the
67
+ /// bytes makes the corpus self-describing instead of requiring every reader
68
+ /// to own a canonical encoder.
69
+ #[serde(default)]
70
+ canonical_input: Option<String>,
71
+ /// The canonical output both implementations must produce.
72
+ #[serde(default)]
73
+ expected_output: Option<String>,
74
+ }
75
+
76
+ impl Case {
77
+ /// The exact bytes this case presents to a contract implementation.
78
+ fn bytes(&self) -> Vec<u8> {
79
+ match (&self.input, &self.raw_input) {
80
+ (Some(input), None) => canonical_input_bytes(input),
81
+ (None, Some(raw)) => raw.as_bytes().to_vec(),
82
+ _ => panic!(
83
+ "case {} must have exactly one of input/raw_input",
84
+ self.name
85
+ ),
86
+ }
87
+ }
88
+
89
+ fn expected(&self) -> &str {
90
+ self.expected_output.as_deref().unwrap_or_else(|| {
91
+ panic!(
92
+ "case {} has no expected_output; regenerate the corpus with \
93
+ FELTDB_TRIGGER_CONTRACT_BLESS=1",
94
+ self.name
95
+ )
96
+ })
97
+ }
98
+ }
99
+
100
+ fn corpus_path() -> PathBuf {
101
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/trigger_contract_corpus.json")
102
+ }
103
+
104
+ fn corpus() -> &'static Corpus {
105
+ static CORPUS: OnceLock<Corpus> = OnceLock::new();
106
+ CORPUS.get_or_init(|| {
107
+ let bytes = std::fs::read(corpus_path()).expect("conformance corpus is committed");
108
+ serde_json::from_slice(&bytes).expect("conformance corpus is well formed")
109
+ })
110
+ }
111
+
112
+ #[test]
113
+ fn corpus_addresses_this_contract_at_this_version() {
114
+ let corpus = corpus();
115
+ assert_eq!(corpus.corpus_version, 1);
116
+ assert_eq!(corpus.contract, TRIGGER_MATCH_CONTRACT_ID);
117
+ assert_eq!(corpus.contract_version, TRIGGER_MATCH_CONTRACT_VERSION);
118
+ assert!(
119
+ corpus.cases.len() >= 90,
120
+ "the corpus is the proof; it should not shrink silently"
121
+ );
122
+ let mut names: Vec<&str> = corpus.cases.iter().map(|case| case.name.as_str()).collect();
123
+ names.sort_unstable();
124
+ let unique = names.len();
125
+ names.dedup();
126
+ assert_eq!(unique, names.len(), "case names identify cases");
127
+ }
128
+
129
+ /// Every required category of behaviour is represented by at least one case.
130
+ ///
131
+ /// This is a guard against the corpus quietly losing a dimension it was built
132
+ /// to cover, not a substitute for reading it.
133
+ #[test]
134
+ fn corpus_covers_every_required_category() {
135
+ let names: Vec<&str> = corpus().cases.iter().map(|c| c.name.as_str()).collect();
136
+ for required in [
137
+ "collection_matches",
138
+ "collection_differs",
139
+ "change_kind_update_matches",
140
+ "change_kind_insert_against_update_trigger",
141
+ "disabled_trigger_rejects_matching_change",
142
+ "predicate_matches_string",
143
+ "predicate_value_mismatch",
144
+ "predicate_absent_matches_any_value",
145
+ "value_null_present_with_whole_record_predicate",
146
+ "nested_path_matches",
147
+ "array_equality_matches",
148
+ "integer_equality_matches",
149
+ "fanout_first_trigger_matches",
150
+ "malformed_not_json",
151
+ "empty_object_record",
152
+ ] {
153
+ assert!(
154
+ names.contains(&required),
155
+ "corpus lost coverage: {required}"
156
+ );
157
+ }
158
+ }
159
+
160
+ /// The committed canonical bytes are what this crate's encoder produces.
161
+ ///
162
+ /// Consumers outside Rust read `canonical_input` rather than re-canonicalizing,
163
+ /// so this is the check that keeps those bytes honest: if canonicalization
164
+ /// changed, every other reader of the corpus would silently be testing an
165
+ /// encoding FeltDB no longer emits.
166
+ #[test]
167
+ fn committed_canonical_bytes_match_the_encoder() {
168
+ for case in &corpus().cases {
169
+ match (&case.input, &case.raw_input) {
170
+ (Some(input), None) => assert_eq!(
171
+ case.canonical_input.as_deref(),
172
+ Some(
173
+ String::from_utf8(canonical_input_bytes(input))
174
+ .unwrap()
175
+ .as_str()
176
+ ),
177
+ "{} has a stale canonical_input",
178
+ case.name
179
+ ),
180
+ (None, Some(_)) => assert!(
181
+ case.canonical_input.is_none(),
182
+ "{} is a raw case and must not also record canonical_input",
183
+ case.name
184
+ ),
185
+ _ => panic!(
186
+ "case {} must have exactly one of input/raw_input",
187
+ case.name
188
+ ),
189
+ }
190
+ }
191
+ }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // WASM host
195
+ // ---------------------------------------------------------------------------
196
+
197
+ const WASM_CRATE: &str = "crates/feltdb-trigger-contract-wasm";
198
+ const WASM_ARTIFACT: &str =
199
+ "target/wasm32-unknown-unknown/release/feltdb_trigger_contract_wasm.wasm";
200
+
201
+ fn repository_root() -> &'static Path {
202
+ Path::new(env!("CARGO_MANIFEST_DIR"))
203
+ .parent()
204
+ .and_then(Path::parent)
205
+ .expect("the crate lives at <root>/crates/feltdb")
206
+ }
207
+
208
+ /// Whether this machine can compile the contract to WebAssembly at all.
209
+ fn wasm_target_installed() -> bool {
210
+ Command::new("rustc")
211
+ .args([
212
+ "--print",
213
+ "target-libdir",
214
+ "--target",
215
+ "wasm32-unknown-unknown",
216
+ ])
217
+ .output()
218
+ .is_ok_and(|output| output.status.success())
219
+ }
220
+
221
+ /// Compiles a WASM contract crate and returns the module bytes.
222
+ ///
223
+ /// A build failure is a defect and panics. A missing `wasm32-unknown-unknown`
224
+ /// target is an environment fact, reported by the caller as a skip.
225
+ fn build_module(manifest_dir: &Path) -> Vec<u8> {
226
+ let output = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
227
+ .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
228
+ .current_dir(manifest_dir)
229
+ // The contract crate is its own workspace; inheriting the outer
230
+ // build's target directory or profile would defeat that.
231
+ .env_remove("CARGO_TARGET_DIR")
232
+ .env_remove("RUSTFLAGS")
233
+ .output()
234
+ .expect("cargo is runnable");
235
+ assert!(
236
+ output.status.success(),
237
+ "building the WASM contract at {} failed:\n{}",
238
+ manifest_dir.display(),
239
+ String::from_utf8_lossy(&output.stderr)
240
+ );
241
+ std::fs::read(manifest_dir.join(WASM_ARTIFACT)).expect("the build emits one module")
242
+ }
243
+
244
+ /// The committed WASM contract, compiled once for the whole suite.
245
+ fn contract_module() -> Option<&'static [u8]> {
246
+ static MODULE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
247
+ MODULE
248
+ .get_or_init(|| {
249
+ if let Ok(path) = std::env::var("FELTDB_TRIGGER_CONTRACT_WASM") {
250
+ return Some(
251
+ std::fs::read(path).expect("FELTDB_TRIGGER_CONTRACT_WASM is readable"),
252
+ );
253
+ }
254
+ if !wasm_target_installed() {
255
+ return None;
256
+ }
257
+ Some(build_module(&repository_root().join(WASM_CRATE)))
258
+ })
259
+ .as_deref()
260
+ }
261
+
262
+ /// Announces a skip loudly, so an absent toolchain is never a silent pass.
263
+ fn skipped(test: &str) {
264
+ eprintln!(
265
+ "SKIPPED {test}: the wasm32-unknown-unknown target is not installed, so the WASM \
266
+ contract implementation could not be built. Native conformance against the committed \
267
+ expectations still ran. Install the target, or set FELTDB_TRIGGER_CONTRACT_WASM to a \
268
+ prebuilt module, to execute the cross-runtime proof."
269
+ );
270
+ }
271
+
272
+ /// One instantiation of the WASM contract.
273
+ ///
274
+ /// The linker is empty and stays empty: the module declares no imports, so
275
+ /// there is nothing to satisfy and nothing to grant.
276
+ struct WasmContract {
277
+ store: wasmi::Store<()>,
278
+ memory: wasmi::Memory,
279
+ alloc: wasmi::TypedFunc<u32, u32>,
280
+ evaluate: wasmi::TypedFunc<(u32, u32), u64>,
281
+ free: wasmi::TypedFunc<(u32, u32), ()>,
282
+ }
283
+
284
+ impl WasmContract {
285
+ fn instantiate(bytes: &[u8]) -> Self {
286
+ let engine = wasmi::Engine::default();
287
+ let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
288
+ let mut store = wasmi::Store::new(&engine, ());
289
+ let instance = wasmi::Linker::<()>::new(&engine)
290
+ .instantiate_and_start(&mut store, &module)
291
+ .expect("an empty linker satisfies a module with no imports");
292
+ Self {
293
+ memory: instance
294
+ .get_memory(&store, "memory")
295
+ .expect("the module exports its memory"),
296
+ alloc: instance
297
+ .get_typed_func(&store, "contract_alloc")
298
+ .expect("contract_alloc"),
299
+ evaluate: instance
300
+ .get_typed_func(&store, "contract_evaluate")
301
+ .expect("contract_evaluate"),
302
+ free: instance
303
+ .get_typed_func(&store, "contract_free")
304
+ .expect("contract_free"),
305
+ store,
306
+ }
307
+ }
308
+
309
+ fn version(bytes: &[u8]) -> u32 {
310
+ let engine = wasmi::Engine::default();
311
+ let module = wasmi::Module::new(&engine, bytes).unwrap();
312
+ let mut store = wasmi::Store::new(&engine, ());
313
+ let instance = wasmi::Linker::<()>::new(&engine)
314
+ .instantiate_and_start(&mut store, &module)
315
+ .unwrap();
316
+ instance
317
+ .get_typed_func::<(), u32>(&store, "contract_version")
318
+ .expect("contract_version")
319
+ .call(&mut store, ())
320
+ .expect("contract_version is total")
321
+ }
322
+
323
+ fn evaluate(&mut self, input: &[u8]) -> Vec<u8> {
324
+ let len = input.len() as u32;
325
+ let ptr = self.alloc.call(&mut self.store, len).expect("allocation");
326
+ self.memory
327
+ .write(&mut self.store, ptr as usize, input)
328
+ .expect("input fits in linear memory");
329
+ let packed = self
330
+ .evaluate
331
+ .call(&mut self.store, (ptr, len))
332
+ .expect("the contract never traps on any input");
333
+ let out_ptr = (packed >> 32) as usize;
334
+ let out_len = (packed & 0xffff_ffff) as usize;
335
+ let mut output = vec![0u8; out_len];
336
+ self.memory
337
+ .read(&self.store, out_ptr, &mut output)
338
+ .expect("the output buffer is inside linear memory");
339
+ self.free.call(&mut self.store, (ptr, len)).expect("free");
340
+ self.free
341
+ .call(&mut self.store, (out_ptr as u32, out_len as u32))
342
+ .expect("free");
343
+ output
344
+ }
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Conformance
349
+ // ---------------------------------------------------------------------------
350
+
351
+ /// Native evaluation reproduces the committed canonical output for every case.
352
+ ///
353
+ /// The expectations were recorded from a run in which the native and the WASM
354
+ /// implementation already agreed, so this is a full-strength check of native
355
+ /// semantics even on a machine with no WASM toolchain.
356
+ #[test]
357
+ fn native_matches_the_committed_expectations() {
358
+ let mut divergent = Vec::new();
359
+ for case in &corpus().cases {
360
+ let produced = String::from_utf8(evaluate_canonical(&case.bytes())).unwrap();
361
+ if produced != case.expected() {
362
+ divergent.push(format!(
363
+ "{}\n expected: {}\n native: {}",
364
+ case.name,
365
+ case.expected(),
366
+ produced
367
+ ));
368
+ }
369
+ }
370
+ assert!(
371
+ divergent.is_empty(),
372
+ "native contract diverged from the committed expectations:\n{}",
373
+ divergent.join("\n")
374
+ );
375
+ }
376
+
377
+ /// The proof: one corpus, two implementations, byte-identical canonical output.
378
+ #[test]
379
+ fn native_and_wasm_agree_on_every_fixture() {
380
+ let Some(bytes) = contract_module() else {
381
+ return skipped("native_and_wasm_agree_on_every_fixture");
382
+ };
383
+ let mut wasm = WasmContract::instantiate(bytes);
384
+
385
+ println!(
386
+ "{:<52} {:<7} {:<7} {}",
387
+ "fixture", "native", "wasm", "equal"
388
+ );
389
+ let mut divergent = Vec::new();
390
+ for case in &corpus().cases {
391
+ let input = case.bytes();
392
+ let native = evaluate_canonical(&input);
393
+ let produced = wasm.evaluate(&input);
394
+ let equal = native == produced;
395
+ println!(
396
+ "{:<52} {:<7} {:<7} {}",
397
+ case.name,
398
+ summarize(&native),
399
+ summarize(&produced),
400
+ if equal { "yes" } else { "NO" }
401
+ );
402
+ if !equal {
403
+ divergent.push(format!(
404
+ "{}\n input: {}\n native: {}\n wasm: {}",
405
+ case.name,
406
+ String::from_utf8_lossy(&input),
407
+ String::from_utf8_lossy(&native),
408
+ String::from_utf8_lossy(&produced)
409
+ ));
410
+ }
411
+ }
412
+ assert!(
413
+ divergent.is_empty(),
414
+ "native and WASM contract implementations diverged:\n{}",
415
+ divergent.join("\n")
416
+ );
417
+ }
418
+
419
+ /// The WASM implementation reproduces the committed canonical output too.
420
+ ///
421
+ /// Separate from the pairwise comparison on purpose: two implementations can
422
+ /// agree with each other while both having moved.
423
+ #[test]
424
+ fn wasm_matches_the_committed_expectations() {
425
+ let Some(bytes) = contract_module() else {
426
+ return skipped("wasm_matches_the_committed_expectations");
427
+ };
428
+ let mut wasm = WasmContract::instantiate(bytes);
429
+ for case in &corpus().cases {
430
+ assert_eq!(
431
+ String::from_utf8(wasm.evaluate(&case.bytes())).unwrap(),
432
+ case.expected(),
433
+ "WASM contract diverged from the committed expectation for {}",
434
+ case.name
435
+ );
436
+ }
437
+ }
438
+
439
+ /// Both implementations answer for the same contract at the same version.
440
+ #[test]
441
+ fn both_implementations_declare_the_same_contract_version() {
442
+ let Some(bytes) = contract_module() else {
443
+ return skipped("both_implementations_declare_the_same_contract_version");
444
+ };
445
+ assert_eq!(WasmContract::version(bytes), TRIGGER_MATCH_CONTRACT_VERSION);
446
+ for case in &corpus().cases {
447
+ let output: Value = serde_json::from_slice(&evaluate_canonical(&case.bytes())).unwrap();
448
+ assert_eq!(output["contract"], TRIGGER_MATCH_CONTRACT_ID);
449
+ assert_eq!(output["version"], TRIGGER_MATCH_CONTRACT_VERSION);
450
+ }
451
+ }
452
+
453
+ /// A short rendering of an output, for the printed conformance table.
454
+ fn summarize(output: &[u8]) -> String {
455
+ let parsed: Value = match serde_json::from_slice(output) {
456
+ Ok(value) => value,
457
+ Err(_) => return "?".into(),
458
+ };
459
+ if let Some(matched) = parsed["outcome"]["matched"].as_bool() {
460
+ return if matched { "match" } else { "no-match" }.into();
461
+ }
462
+ "error".into()
463
+ }
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Determinism
467
+ // ---------------------------------------------------------------------------
468
+
469
+ /// The same input, in the same instance, produces the same output every time.
470
+ #[test]
471
+ fn wasm_is_deterministic_across_repeated_calls() {
472
+ let Some(bytes) = contract_module() else {
473
+ return skipped("wasm_is_deterministic_across_repeated_calls");
474
+ };
475
+ let mut wasm = WasmContract::instantiate(bytes);
476
+ for case in &corpus().cases {
477
+ let input = case.bytes();
478
+ let first = wasm.evaluate(&input);
479
+ for round in 1..16 {
480
+ assert_eq!(
481
+ wasm.evaluate(&input),
482
+ first,
483
+ "{} changed answer on repeat {round} within one instance",
484
+ case.name
485
+ );
486
+ }
487
+ }
488
+ }
489
+
490
+ /// The same input produces the same output in a fresh instance.
491
+ ///
492
+ /// Each instance gets its own `Store`, so its linear memory, allocator state
493
+ /// and call history all start over. An implementation that carried state
494
+ /// between decisions would show it here.
495
+ #[test]
496
+ fn wasm_is_deterministic_across_fresh_instances() {
497
+ let Some(bytes) = contract_module() else {
498
+ return skipped("wasm_is_deterministic_across_fresh_instances");
499
+ };
500
+ let mut baseline = WasmContract::instantiate(bytes);
501
+ let expected: Vec<Vec<u8>> = corpus()
502
+ .cases
503
+ .iter()
504
+ .map(|case| baseline.evaluate(&case.bytes()))
505
+ .collect();
506
+
507
+ for round in 0..4 {
508
+ let mut fresh = WasmContract::instantiate(bytes);
509
+ for (case, want) in corpus().cases.iter().zip(&expected) {
510
+ assert_eq!(
511
+ &fresh.evaluate(&case.bytes()),
512
+ want,
513
+ "{} changed answer in fresh instance {round}",
514
+ case.name
515
+ );
516
+ }
517
+ }
518
+ }
519
+
520
+ /// Order of evaluation does not affect any answer.
521
+ ///
522
+ /// The corpus is replayed forwards and backwards through one instance. A
523
+ /// decision that depended on what had been evaluated before would differ.
524
+ #[test]
525
+ fn wasm_answers_do_not_depend_on_evaluation_order() {
526
+ let Some(bytes) = contract_module() else {
527
+ return skipped("wasm_answers_do_not_depend_on_evaluation_order");
528
+ };
529
+ let mut forwards = WasmContract::instantiate(bytes);
530
+ let ordered: Vec<Vec<u8>> = corpus()
531
+ .cases
532
+ .iter()
533
+ .map(|case| forwards.evaluate(&case.bytes()))
534
+ .collect();
535
+
536
+ let mut backwards = WasmContract::instantiate(bytes);
537
+ for (index, case) in corpus().cases.iter().enumerate().rev() {
538
+ assert_eq!(
539
+ backwards.evaluate(&case.bytes()),
540
+ ordered[index],
541
+ "{} answered differently when the corpus ran in reverse",
542
+ case.name
543
+ );
544
+ }
545
+ }
546
+
547
+ /// The same input produces the same output in a separate OS process.
548
+ ///
549
+ /// `cargo test` may run the tests above in one process on one thread pool.
550
+ /// This re-runs the corpus in a child process with an independently created
551
+ /// engine, which is the strongest process boundary this harness can establish
552
+ /// without shipping a second binary.
553
+ #[test]
554
+ fn wasm_is_deterministic_across_process_boundaries() {
555
+ let Some(bytes) = contract_module() else {
556
+ return skipped("wasm_is_deterministic_across_process_boundaries");
557
+ };
558
+ let mut here = WasmContract::instantiate(bytes);
559
+ let mine: Vec<Vec<u8>> = corpus()
560
+ .cases
561
+ .iter()
562
+ .map(|case| here.evaluate(&case.bytes()))
563
+ .collect();
564
+
565
+ let executable = std::env::current_exe().expect("the test binary has a path");
566
+ let output = Command::new(executable)
567
+ .args([
568
+ "--exact",
569
+ "wasm_is_deterministic_across_fresh_instances",
570
+ "--nocapture",
571
+ ])
572
+ .output()
573
+ .expect("the test binary re-runs");
574
+ assert!(
575
+ output.status.success(),
576
+ "the corpus did not reproduce in a separate process:\n{}",
577
+ String::from_utf8_lossy(&output.stdout)
578
+ );
579
+
580
+ // And the parent's own answers still match the committed expectations, so
581
+ // the child agreeing with itself is not the whole claim.
582
+ for (case, produced) in corpus().cases.iter().zip(&mine) {
583
+ assert_eq!(String::from_utf8_lossy(produced), case.expected());
584
+ }
585
+ }
586
+
587
+ // ---------------------------------------------------------------------------
588
+ // Runtime restrictions
589
+ // ---------------------------------------------------------------------------
590
+
591
+ /// The module imports nothing.
592
+ ///
593
+ /// This is the whole capability argument and it is deliberately narrow. A
594
+ /// WebAssembly module reaches the outside world only through its imports.
595
+ /// With an empty import section there is no clock, no filesystem handle, no
596
+ /// socket and no entropy source in scope — not because they were denied at
597
+ /// call time, but because no such function is linked. It says nothing about
598
+ /// hostile modules, which is a different claim this suite does not make.
599
+ #[test]
600
+ fn wasm_module_declares_no_imports() {
601
+ let Some(bytes) = contract_module() else {
602
+ return skipped("wasm_module_declares_no_imports");
603
+ };
604
+ let engine = wasmi::Engine::default();
605
+ let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
606
+ let imports: Vec<String> = module
607
+ .imports()
608
+ .map(|import: wasmi::ImportType| format!("{}::{}", import.module(), import.name()))
609
+ .collect();
610
+ assert!(
611
+ imports.is_empty(),
612
+ "the contract module must import nothing; it imports {imports:?}"
613
+ );
614
+ }
615
+
616
+ /// An empty linker instantiates the module.
617
+ ///
618
+ /// The companion to the import check, from the host's side: nothing is
619
+ /// provided, and instantiation still succeeds, so no capability is being
620
+ /// supplied implicitly by the runtime.
621
+ #[test]
622
+ fn wasm_module_instantiates_with_no_host_functions() {
623
+ let Some(bytes) = contract_module() else {
624
+ return skipped("wasm_module_instantiates_with_no_host_functions");
625
+ };
626
+ let engine = wasmi::Engine::default();
627
+ let module = wasmi::Module::new(&engine, bytes).unwrap();
628
+ let mut store = wasmi::Store::new(&engine, ());
629
+ wasmi::Linker::<()>::new(&engine)
630
+ .instantiate_and_start(&mut store, &module)
631
+ .expect("no host function is required");
632
+ }
633
+
634
+ /// The module exports the contract ABI and nothing that could act on FeltDB.
635
+ ///
636
+ /// The exported surface is memory plus four functions. There is no export that
637
+ /// writes state, creates work, or names a FeltDB concept: the module's only
638
+ /// possible effect on FeltDB is the bytes it returns.
639
+ #[test]
640
+ fn wasm_module_exports_only_the_contract_abi() {
641
+ let Some(bytes) = contract_module() else {
642
+ return skipped("wasm_module_exports_only_the_contract_abi");
643
+ };
644
+ let engine = wasmi::Engine::default();
645
+ let module = wasmi::Module::new(&engine, bytes).unwrap();
646
+ let mut functions: Vec<String> = module
647
+ .exports()
648
+ .filter(|export| export.ty().func().is_some())
649
+ .map(|export| export.name().to_string())
650
+ .collect();
651
+ functions.sort();
652
+ assert_eq!(
653
+ functions,
654
+ vec![
655
+ "contract_alloc".to_string(),
656
+ "contract_evaluate".to_string(),
657
+ "contract_free".to_string(),
658
+ "contract_version".to_string(),
659
+ ]
660
+ );
661
+ }
662
+
663
+ /// WASI is not present, under any of the names a runtime would look for.
664
+ #[test]
665
+ fn wasm_module_requires_no_wasi() {
666
+ let Some(bytes) = contract_module() else {
667
+ return skipped("wasm_module_requires_no_wasi");
668
+ };
669
+ let text = String::from_utf8_lossy(bytes);
670
+ for forbidden in [
671
+ "wasi_snapshot_preview1",
672
+ "wasi_unstable",
673
+ "clock_time_get",
674
+ "random_get",
675
+ "fd_read",
676
+ "fd_write",
677
+ "sock_recv",
678
+ "path_open",
679
+ ] {
680
+ assert!(
681
+ !text.contains(forbidden),
682
+ "the contract module names {forbidden}, which it has no reason to"
683
+ );
684
+ }
685
+ }
686
+
687
+ /// The contract never traps, on any corpus input.
688
+ ///
689
+ /// A trap would be a runtime-specific outcome rather than a contract decision,
690
+ /// and the two implementations could not be compared through it.
691
+ #[test]
692
+ fn wasm_contract_returns_a_decision_for_every_input_including_malformed() {
693
+ let Some(bytes) = contract_module() else {
694
+ return skipped("wasm_contract_returns_a_decision_for_every_input_including_malformed");
695
+ };
696
+ let mut wasm = WasmContract::instantiate(bytes);
697
+ for case in &corpus().cases {
698
+ let output = wasm.evaluate(&case.bytes());
699
+ let parsed: Value =
700
+ serde_json::from_slice(&output).expect("every output is canonical JSON");
701
+ assert_eq!(parsed["contract"], TRIGGER_MATCH_CONTRACT_ID);
702
+ assert!(
703
+ parsed.get("outcome").is_some() ^ parsed.get("error").is_some(),
704
+ "{} produced neither or both of outcome and error",
705
+ case.name
706
+ );
707
+ }
708
+ // Inputs no fixture would contain, to show the trap-freedom is not a
709
+ // property of the corpus.
710
+ for hostile in [
711
+ vec![0u8; 0],
712
+ vec![0xff; 64],
713
+ b"{".repeat(4096),
714
+ b"[".repeat(4096),
715
+ vec![b'"'; 1024],
716
+ ] {
717
+ let output = wasm.evaluate(&hostile);
718
+ assert!(serde_json::from_slice::<Value>(&output).is_ok());
719
+ }
720
+ }
721
+
722
+ // ---------------------------------------------------------------------------
723
+ // Mutation: does the suite actually detect divergence?
724
+ // ---------------------------------------------------------------------------
725
+
726
+ /// A deliberate change to the WASM implementation's source.
727
+ struct Mutation {
728
+ name: &'static str,
729
+ file: &'static str,
730
+ from: &'static str,
731
+ to: &'static str,
732
+ }
733
+
734
+ const MUTATIONS: &[Mutation] = &[
735
+ Mutation {
736
+ name: "predicate comparison always succeeds",
737
+ file: "src/lib.rs",
738
+ from: " if !resolved.equals(predicate.equals) {",
739
+ to: " if false {",
740
+ },
741
+ Mutation {
742
+ name: "collection comparison is case-insensitive",
743
+ file: "src/lib.rs",
744
+ from: " if change.collection != trigger.source_collection {",
745
+ to: " if !change.collection.eq_ignore_ascii_case(trigger.source_collection) {",
746
+ },
747
+ Mutation {
748
+ name: "disabled triggers are accepted",
749
+ file: "src/lib.rs",
750
+ from: " if !trigger.enabled {",
751
+ to: " if false {",
752
+ },
753
+ Mutation {
754
+ name: "a null record value counts as absent",
755
+ file: "src/lib.rs",
756
+ from: " let Some(value) = change.value else {",
757
+ to: " let Some(value) = change.value.filter(|value| **value != Json::Null) else {",
758
+ },
759
+ Mutation {
760
+ name: "change kind is never compared",
761
+ file: "src/lib.rs",
762
+ from: " if change.change_kind != trigger.change_kind {",
763
+ to: " if false {",
764
+ },
765
+ Mutation {
766
+ name: "output encoding renames a member",
767
+ file: "src/lib.rs",
768
+ from: r#"out.extend_from_slice(b"\",\"outcome\":{\"matched\":");"#,
769
+ to: r#"out.extend_from_slice(b"\",\"result\":{\"matched\":");"#,
770
+ },
771
+ Mutation {
772
+ name: "integers and floats compare equal",
773
+ file: "src/json.rs",
774
+ from: " (Json::Float(left), Json::Float(right)) => left == right,",
775
+ to: " (Json::Float(left), Json::Float(right)) => left == right,\n (Json::PosInt(left), Json::Float(right)) => *left as f64 == *right,\n (Json::Float(left), Json::PosInt(right)) => *left == *right as f64,",
776
+ },
777
+ ];
778
+
779
+ /// Every deliberate mutation of the WASM contract is caught by the corpus.
780
+ ///
781
+ /// Without this the conformance suite proves only that two implementations
782
+ /// agree, not that agreement was ever in question. Each mutant is built from a
783
+ /// copy of the real source with one edit applied, and must fail against the
784
+ /// committed expectations.
785
+ #[test]
786
+ fn deliberate_wasm_mutations_are_detected() {
787
+ if contract_module().is_none() {
788
+ return skipped("deliberate_wasm_mutations_are_detected");
789
+ }
790
+ let source = repository_root().join(WASM_CRATE);
791
+ let scratch = tempfile::tempdir().expect("a scratch directory");
792
+
793
+ let mut undetected = Vec::new();
794
+ for mutation in MUTATIONS {
795
+ let root = scratch.path().join(mutation.name.replace(' ', "-"));
796
+ copy_crate(&source, &root);
797
+ let target = root.join(mutation.file);
798
+ let original = std::fs::read_to_string(&target).expect("the mutated file exists");
799
+ assert!(
800
+ original.matches(mutation.from).count() == 1,
801
+ "mutation {:?} no longer applies: its anchor is not present exactly once in {}. \
802
+ The mutation suite must be updated with the implementation it mutates.",
803
+ mutation.name,
804
+ mutation.file
805
+ );
806
+ std::fs::write(&target, original.replace(mutation.from, mutation.to)).unwrap();
807
+
808
+ let mut mutant = WasmContract::instantiate(&build_module(&root));
809
+ let caught: Vec<&str> = corpus()
810
+ .cases
811
+ .iter()
812
+ .filter(|case| {
813
+ String::from_utf8_lossy(&mutant.evaluate(&case.bytes())) != case.expected()
814
+ })
815
+ .map(|case| case.name.as_str())
816
+ .collect();
817
+ match caught.first() {
818
+ Some(first) => println!(
819
+ "mutation {:?} caught by {} fixtures, first: {first}",
820
+ mutation.name,
821
+ caught.len()
822
+ ),
823
+ None => undetected.push(mutation.name),
824
+ }
825
+ }
826
+ assert!(
827
+ undetected.is_empty(),
828
+ "the corpus does not distinguish these mutations from correct behaviour: {undetected:?}"
829
+ );
830
+ }
831
+
832
+ /// Copies the contract crate's source, without its build directory.
833
+ fn copy_crate(from: &Path, to: &Path) {
834
+ std::fs::create_dir_all(to.join("src")).unwrap();
835
+ std::fs::copy(from.join("Cargo.toml"), to.join("Cargo.toml")).unwrap();
836
+ for entry in std::fs::read_dir(from.join("src")).unwrap() {
837
+ let entry = entry.unwrap();
838
+ std::fs::copy(entry.path(), to.join("src").join(entry.file_name())).unwrap();
839
+ }
840
+ }
841
+
842
+ /// Mutating the canonical encoding is detected where the encoding is
843
+ /// semantically load-bearing, and not where it is not.
844
+ ///
845
+ /// This is the honest shape of the result. Three encoder mutations are applied
846
+ /// to well-formed cases and fed to both implementations:
847
+ ///
848
+ /// - conflating an absent record value with `null` changes a decision, and
849
+ /// both implementations change it together, so only the committed
850
+ /// expectations catch it;
851
+ /// - dropping the contract identity turns every case into an input error,
852
+ /// likewise caught by the expectations; and
853
+ /// - emitting members in non-sorted order changes nothing, because JSON object
854
+ /// equality is by member and both parsers agree. The suite correctly does
855
+ /// not flag it, and the contract does not claim byte-canonical input.
856
+ #[test]
857
+ fn canonicalization_mutations_are_detected_where_they_matter() {
858
+ let cases: Vec<(&Case, Value)> = corpus()
859
+ .cases
860
+ .iter()
861
+ .filter_map(|case| {
862
+ let input = case.input.as_ref()?;
863
+ Some((case, serde_json::to_value(input).unwrap()))
864
+ })
865
+ .collect();
866
+ assert!(cases.len() > 40, "most of the corpus is typed input");
867
+
868
+ // 1. Absence conflated with null.
869
+ let mut changed = 0;
870
+ for (case, encoded) in &cases {
871
+ let mut mutated = encoded.clone();
872
+ if mutated["change"].get("value").is_none() {
873
+ mutated["change"]["value"] = Value::Null;
874
+ }
875
+ let produced = String::from_utf8(evaluate_canonical(
876
+ serde_json::to_vec(&mutated).unwrap().as_slice(),
877
+ ))
878
+ .unwrap();
879
+ if produced != case.expected() {
880
+ changed += 1;
881
+ }
882
+ }
883
+ assert!(
884
+ changed > 0,
885
+ "an encoder that writes null for an absent record value must change a decision"
886
+ );
887
+
888
+ // 2. Contract identity dropped.
889
+ for (case, encoded) in cases.iter().take(1) {
890
+ let mut mutated = encoded.clone();
891
+ mutated.as_object_mut().unwrap().remove("contract");
892
+ let produced = String::from_utf8(evaluate_canonical(
893
+ serde_json::to_vec(&mutated).unwrap().as_slice(),
894
+ ))
895
+ .unwrap();
896
+ assert_ne!(produced, case.expected());
897
+ assert!(produced.contains("CONTRACT_INPUT_INVALID"));
898
+ }
899
+
900
+ // 3. Member order, which carries no meaning.
901
+ for (case, encoded) in &cases {
902
+ let produced =
903
+ String::from_utf8(evaluate_canonical(encode_reversed(encoded).as_bytes())).unwrap();
904
+ assert_eq!(
905
+ produced,
906
+ case.expected(),
907
+ "{} changed answer under a reordered encoding; object member order is \
908
+ not part of this contract's semantics",
909
+ case.name
910
+ );
911
+ }
912
+ }
913
+
914
+ /// Encodes a value with every object's members in reverse sorted order.
915
+ ///
916
+ /// `serde_json::Map` re-sorts on insert, so a non-canonical member order has to
917
+ /// be produced as text rather than as a value.
918
+ fn encode_reversed(value: &Value) -> String {
919
+ match value {
920
+ Value::Object(members) => {
921
+ let mut parts: Vec<String> = members
922
+ .iter()
923
+ .map(|(key, value)| {
924
+ format!(
925
+ "{}:{}",
926
+ serde_json::to_string(key).unwrap(),
927
+ encode_reversed(value)
928
+ )
929
+ })
930
+ .collect();
931
+ parts.reverse();
932
+ format!("{{{}}}", parts.join(","))
933
+ }
934
+ Value::Array(items) => format!(
935
+ "[{}]",
936
+ items
937
+ .iter()
938
+ .map(encode_reversed)
939
+ .collect::<Vec<_>>()
940
+ .join(",")
941
+ ),
942
+ other => serde_json::to_string(other).unwrap(),
943
+ }
944
+ }
945
+
946
+ // ---------------------------------------------------------------------------
947
+ // Corpus maintenance
948
+ // ---------------------------------------------------------------------------
949
+
950
+ /// Records the canonical output of every case, after both implementations agree.
951
+ ///
952
+ /// Ignored by default: it writes the corpus. Run it deliberately with
953
+ /// `FELTDB_TRIGGER_CONTRACT_BLESS=1 cargo test -p feltdb --test
954
+ /// trigger_contract_conformance -- bless --ignored`. It refuses to record
955
+ /// anything unless the WASM implementation is available and agrees, so an
956
+ /// expectation can never be blessed from one implementation alone.
957
+ #[test]
958
+ #[ignore = "rewrites the committed corpus"]
959
+ fn bless() {
960
+ assert_eq!(
961
+ std::env::var("FELTDB_TRIGGER_CONTRACT_BLESS").as_deref(),
962
+ Ok("1"),
963
+ "set FELTDB_TRIGGER_CONTRACT_BLESS=1 to rewrite the corpus"
964
+ );
965
+ let bytes = contract_module().expect(
966
+ "blessing requires the WASM implementation: an expectation recorded from the native \
967
+ implementation alone would prove nothing",
968
+ );
969
+ let mut wasm = WasmContract::instantiate(bytes);
970
+
971
+ let mut document: Value =
972
+ serde_json::from_slice(&std::fs::read(corpus_path()).unwrap()).unwrap();
973
+ for (index, case) in corpus().cases.iter().enumerate() {
974
+ let input = case.bytes();
975
+ let native = evaluate_canonical(&input);
976
+ let produced = wasm.evaluate(&input);
977
+ assert_eq!(
978
+ String::from_utf8_lossy(&native),
979
+ String::from_utf8_lossy(&produced),
980
+ "refusing to record {}: the implementations disagree",
981
+ case.name
982
+ );
983
+ if case.input.is_some() {
984
+ document["cases"][index]["canonical_input"] =
985
+ Value::String(String::from_utf8(input).unwrap());
986
+ }
987
+ document["cases"][index]["expected_output"] =
988
+ Value::String(String::from_utf8(native).unwrap());
989
+ }
990
+ let mut text = serde_json::to_string_pretty(&document).unwrap();
991
+ text.push('\n');
992
+ std::fs::write(corpus_path(), text).unwrap();
993
+ println!("recorded {} expectations", corpus().cases.len());
994
+ }