@feltdb/core 0.15.0 → 0.16.0

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 (247) hide show
  1. package/dist/agent-state.d.ts +20 -0
  2. package/dist/agent-state.d.ts.map +1 -1
  3. package/dist/agent-state.js +36 -0
  4. package/dist/build-identity.json +12 -4
  5. package/dist/cli/commands.js +187 -4
  6. package/dist/cli/publish-engine.js +116 -16
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +3 -0
  9. package/dist/create/build-identity.json +12 -4
  10. package/dist/create/managed-account.js +93 -35
  11. package/dist/create/package-versions.js +1 -1
  12. package/dist/create/server-source/Cargo.lock +6 -509
  13. package/dist/create/server-source/Cargo.toml +2 -13
  14. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  15. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +255 -12
  17. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  18. package/dist/db.d.ts +11 -1
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +15 -2
  21. package/dist/error-codes.d.ts +5 -0
  22. package/dist/error-codes.d.ts.map +1 -1
  23. package/dist/error-codes.js +9 -1
  24. package/dist/http-db.d.ts +16 -0
  25. package/dist/http-db.d.ts.map +1 -1
  26. package/dist/http-db.js +36 -6
  27. package/dist/managed-recovery.d.ts +6 -0
  28. package/dist/managed-recovery.d.ts.map +1 -1
  29. package/dist/managed-recovery.js +9 -0
  30. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-DtybUY0q.js} +1 -1
  31. package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
  32. package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
  33. package/dist/studio-app/index.html +1 -1
  34. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  35. package/dist/wasm/package.json +1 -1
  36. package/dist/workload.d.ts +55 -3
  37. package/dist/workload.d.ts.map +1 -1
  38. package/dist/workload.js +23 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  40. package/dist/workspace/workspace-connection.js +14 -3
  41. package/package.json +1 -1
  42. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  43. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  44. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  45. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  46. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  47. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  49. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  51. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  61. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  63. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  64. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  65. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  66. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  67. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  68. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  69. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  70. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  71. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  72. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  73. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  74. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  75. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  76. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  77. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  80. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  83. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  84. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  86. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  87. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  89. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  90. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  91. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  92. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  93. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  94. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  95. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  96. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  97. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  98. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  99. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  100. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  101. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  102. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  103. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  105. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  106. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  107. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  108. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  109. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  110. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  111. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  112. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  113. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  114. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  115. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  116. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  117. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  118. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  119. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  120. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  121. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  122. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  123. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  124. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  125. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  126. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  127. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  128. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  129. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  130. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  131. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  132. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  133. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  134. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  135. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  136. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  137. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  138. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  139. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  140. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  141. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  142. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  143. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  144. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  145. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  146. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  147. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  148. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  149. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  150. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  151. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  152. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  153. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  154. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  155. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  156. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  157. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  158. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  159. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  160. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  161. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  162. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  163. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  164. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  165. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  166. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  167. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  168. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  169. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  170. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  171. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  172. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  173. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  174. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  175. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  176. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  177. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  178. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  179. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  180. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  181. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  182. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  183. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  184. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  185. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  186. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  187. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  188. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  189. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  190. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  191. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  192. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  193. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  194. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  195. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  196. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  197. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  198. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  199. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  200. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  204. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  206. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  207. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  208. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  209. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  210. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  211. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  212. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  213. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  220. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  221. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  222. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  223. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  225. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  226. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  227. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  228. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  229. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  230. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  231. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  232. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  235. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  236. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  237. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  238. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  239. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  240. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  241. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  242. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  243. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  244. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  245. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  246. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  247. package/dist/studio-app/assets/index-1wd0uKua.js +0 -30
@@ -1,2884 +0,0 @@
1
- //! Conformance, determinism and mutation proofs for the
2
- //! `feltdb.agent.transition` deterministic contract.
3
- //!
4
- //! ```text
5
- //! Contract semantics are authoritative.
6
- //! Runtime implementation is replaceable.
7
- //!
8
- //! NativeTransition(input) == WasmTransition(input) == committed expectation
9
- //! ```
10
- //!
11
- //! The corpus in `tests/fixtures/agent_transition_corpus.json` is committed, not
12
- //! generated at test time. It has two parts:
13
- //!
14
- //! * **cases** -- one decision each, including malformed and raw encodings;
15
- //! * **sequences** -- ordered steps against a small state model. A step's
16
- //! `records` are *derived* from the model by the harness, the decision is
17
- //! checked against the committed bytes, and a committed decision is then
18
- //! applied to the model. This validates decisions and state evolution:
19
- //! that the second of two competing claims is refused *because the first
20
- //! changed the state it read*.
21
- //!
22
- //! **A sequence is not a concurrency proof.** Nothing here runs two writers at
23
- //! once, and the WASM module cannot say anything about storage atomicity. The
24
- //! guarded commit's behaviour under real contention is tested against the real
25
- //! transaction mechanism in `agent_state_native.rs`.
26
- //!
27
- //! Three comparisons run over everything: native == expectation; WASM ==
28
- //! expectation (needs `wasm32-unknown-unknown`); native == WASM, byte for byte.
29
- //!
30
- //! To change the corpus, edit the builder at the bottom of this file and run
31
- //!
32
- //! ```text
33
- //! FELTDB_AGENT_TRANSITION_BLESS=1 cargo test -p feltdb \
34
- //! --test agent_transition_contract_conformance -- bless --ignored
35
- //! ```
36
- //!
37
- //! Blessing refuses to record anything unless the WASM implementation is
38
- //! available and already agrees, so an expectation is never recorded from one
39
- //! implementation alone.
40
-
41
- use feltdb::agent_transition_contract::{
42
- evaluate_canonical, AGENT_TRANSITION_CONTRACT_ID, AGENT_TRANSITION_CONTRACT_VERSION,
43
- };
44
- use serde_json::{json, Map, Value};
45
- use std::collections::BTreeMap;
46
- use std::path::{Path, PathBuf};
47
- use std::process::Command;
48
- use std::sync::OnceLock;
49
-
50
- // ---------------------------------------------------------------------------
51
- // Corpus files
52
- // ---------------------------------------------------------------------------
53
-
54
- fn corpus_path() -> PathBuf {
55
- Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/agent_transition_corpus.json")
56
- }
57
-
58
- fn corpus() -> &'static Value {
59
- static CORPUS: OnceLock<Value> = OnceLock::new();
60
- CORPUS.get_or_init(|| {
61
- let bytes = std::fs::read(corpus_path()).expect("the corpus is committed");
62
- serde_json::from_slice(&bytes).expect("the corpus is well formed")
63
- })
64
- }
65
-
66
- fn cases() -> &'static Vec<Value> {
67
- corpus()["cases"].as_array().expect("cases")
68
- }
69
-
70
- fn sequences() -> &'static Vec<Value> {
71
- corpus()["sequences"].as_array().expect("sequences")
72
- }
73
-
74
- fn text<'a>(value: &'a Value, key: &str) -> &'a str {
75
- value[key]
76
- .as_str()
77
- .unwrap_or_else(|| panic!("missing {key} in {value}"))
78
- }
79
-
80
- /// The exact bytes a case presents to an implementation.
81
- fn case_bytes(case: &Value) -> Vec<u8> {
82
- match (case.get("input"), case.get("raw_input")) {
83
- (Some(input), None) => serde_json::to_vec(input).unwrap(),
84
- (None, Some(raw)) => raw.as_str().unwrap().as_bytes().to_vec(),
85
- _ => panic!(
86
- "case {} needs exactly one of input/raw_input",
87
- text(case, "name")
88
- ),
89
- }
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // The sequential state model
94
- // ---------------------------------------------------------------------------
95
-
96
- /// What the authority holds, as far as the contract can see: records with
97
- /// versions. `apply` is a *model* of the guarded commit used to evolve state
98
- /// between sequential steps. It is not the commit mechanism.
99
- #[derive(Default, Clone)]
100
- struct World {
101
- records: BTreeMap<(String, String), (u64, Value)>,
102
- }
103
-
104
- impl World {
105
- fn get(&self, collection: &str, id: &str) -> Option<&(u64, Value)> {
106
- self.records.get(&(collection.to_string(), id.to_string()))
107
- }
108
-
109
- /// An external record, as a tool would have written it: created at version
110
- /// 1, or bumped by one if it already exists.
111
- fn seed(&mut self, collection: &str, id: &str) {
112
- let key = (collection.to_string(), id.to_string());
113
- let next = self.records.get(&key).map_or(1, |(v, _)| v + 1);
114
- self.records.insert(key, (next, json!({"seeded": true})));
115
- }
116
-
117
- fn stored(&self, collection: &str, id: &str) -> Option<Value> {
118
- self.get(collection, id)
119
- .map(|(version, value)| json!({"value": value, "version": version}))
120
- }
121
-
122
- /// The `records` an operation is decided against, derived the way FeltDB's
123
- /// production layer derives them: by reading, never by being told.
124
- fn records_for(&self, operation: &Value) -> Value {
125
- let op = text(operation, "op");
126
- let mut records = Map::new();
127
- let mut put = |name: &str, value: Option<Value>| {
128
- if let Some(value) = value {
129
- records.insert(name.to_string(), value);
130
- }
131
- };
132
- let task_id = match operation.get("task_id").and_then(Value::as_str) {
133
- Some(id) => Some(id.to_string()),
134
- None => operation
135
- .get("handoff_id")
136
- .and_then(Value::as_str)
137
- .and_then(|h| {
138
- self.get("_feltdb.AgentHandoff", h)
139
- .and_then(|(_, v)| v["task_id"].as_str().map(str::to_string))
140
- }),
141
- };
142
- if let Some(id) = &task_id {
143
- put("task", self.stored("_feltdb.AgentTask", id));
144
- }
145
- if let Some(handoff) = operation.get("handoff_id").and_then(Value::as_str) {
146
- put("handoff", self.stored("_feltdb.AgentHandoff", handoff));
147
- }
148
- if let (Some(task), Some(key)) = (
149
- &task_id,
150
- operation.get("idempotency_key").and_then(Value::as_str),
151
- ) {
152
- put(
153
- "intent",
154
- self.stored("_feltdb.AgentIntent", &format!("{task}~{key}")),
155
- );
156
- }
157
- if matches!(op, "checkpoint" | "record_outcome" | "reconcile") {
158
- let seen: Vec<Value> = operation["evidence"]
159
- .as_array()
160
- .unwrap()
161
- .iter()
162
- .filter(|r| r["kind"] == "record")
163
- .filter_map(|r| {
164
- let (c, i) = (text(r, "collection"), text(r, "id"));
165
- self.get(c, i)
166
- .map(|(v, _)| json!({"collection": c, "id": i, "version": v}))
167
- })
168
- .collect();
169
- records.insert("evidence".into(), Value::Array(seen));
170
- }
171
- Value::Object(records)
172
- }
173
-
174
- /// Applies a committed decision, first checking that every precondition it
175
- /// attached holds in the state it was decided against. A contract that
176
- /// emitted an unsatisfiable precondition would fail here.
177
- fn apply(&mut self, now: u64, output: &Value) -> Result<(), String> {
178
- let outcome = &output["outcome"];
179
- if outcome["kind"] != "commit" {
180
- return Ok(());
181
- }
182
- let holds =
183
- |world: &World, collection: &str, id: &str, version: u64, lease: Option<&str>| {
184
- let Some((v, value)) = world.get(collection, id) else {
185
- return false;
186
- };
187
- *v == version
188
- && lease.is_none_or(|l| {
189
- value["lease"]["leaseId"] == l
190
- && value["lease"]["expiresAt"].as_u64().unwrap() > now
191
- })
192
- };
193
- for write in outcome["writes"].as_array().unwrap() {
194
- let (c, i) = (text(write, "collection"), text(write, "id"));
195
- let expect = &write["expect"];
196
- if expect["absent"] == true {
197
- if self.get(c, i).is_some() {
198
- return Err(format!("{c}/{i} was expected absent"));
199
- }
200
- } else if !holds(
201
- self,
202
- c,
203
- i,
204
- expect["version"].as_u64().unwrap(),
205
- expect["lease_id"].as_str(),
206
- ) {
207
- return Err(format!(
208
- "the contract attached a precondition its own input does not satisfy: {write}"
209
- ));
210
- }
211
- }
212
- for guard in outcome["guards"].as_array().unwrap() {
213
- let (c, i) = (text(guard, "collection"), text(guard, "id"));
214
- if !holds(
215
- self,
216
- c,
217
- i,
218
- guard["version"].as_u64().unwrap(),
219
- guard["lease_id"].as_str(),
220
- ) {
221
- return Err(format!("unsatisfied guard {guard}"));
222
- }
223
- }
224
- for write in outcome["writes"].as_array().unwrap() {
225
- let key = (
226
- text(write, "collection").to_string(),
227
- text(write, "id").to_string(),
228
- );
229
- let next = self.records.get(&key).map_or(1, |(v, _)| v + 1);
230
- self.records.insert(key, (next, write["value"].clone()));
231
- }
232
- Ok(())
233
- }
234
- }
235
-
236
- fn step_input(world: &World, step: &Value) -> Value {
237
- json!({
238
- "contract": AGENT_TRANSITION_CONTRACT_ID,
239
- "now_ms": step["now_ms"],
240
- "operation": step["operation"],
241
- "records": world.records_for(&step["operation"]),
242
- "version": AGENT_TRANSITION_CONTRACT_VERSION,
243
- })
244
- }
245
-
246
- /// Runs every sequence with `decide`, handing each step's derived input and
247
- /// decision to `check`. The first divergence from the committed expectation, or
248
- /// from the state model, is returned rather than panicked, so a mutation test
249
- /// can ask "was it caught?" without a caught panic.
250
- fn run_sequences(
251
- mut decide: impl FnMut(&[u8]) -> Vec<u8>,
252
- mut check: impl FnMut(&str, &str, &str, &str),
253
- ) -> Result<(), String> {
254
- for sequence in sequences() {
255
- let mut world = World::default();
256
- for step in sequence["steps"].as_array().unwrap() {
257
- let label = format!("{}/{}", text(sequence, "name"), text(step, "label"));
258
- for seed in step["seed"].as_array().into_iter().flatten() {
259
- world.seed(text(seed, "collection"), text(seed, "id"));
260
- }
261
- let input = serde_json::to_vec(&step_input(&world, step)).unwrap();
262
- let produced = String::from_utf8(decide(&input)).unwrap();
263
- check(
264
- &label,
265
- text(step, "canonical_input"),
266
- std::str::from_utf8(&input).unwrap(),
267
- &produced,
268
- );
269
- if produced != text(step, "expected_output") {
270
- return Err(format!(
271
- "{label}\n expected: {}\n got: {produced}",
272
- text(step, "expected_output")
273
- ));
274
- }
275
- let decision: Value = serde_json::from_str(&produced).unwrap();
276
- world
277
- .apply(step["now_ms"].as_u64().unwrap(), &decision)
278
- .map_err(|e| format!("{label}: {e}"))?;
279
- }
280
- }
281
- Ok(())
282
- }
283
-
284
- // ---------------------------------------------------------------------------
285
- // WASM
286
- // ---------------------------------------------------------------------------
287
-
288
- const WASM_CRATE: &str = "crates/feltdb-agent-transition-contract-wasm";
289
- const WASM_ARTIFACT: &str =
290
- "target/wasm32-unknown-unknown/release/feltdb_agent_transition_contract_wasm.wasm";
291
-
292
- fn repository_root() -> &'static Path {
293
- Path::new(env!("CARGO_MANIFEST_DIR"))
294
- .parent()
295
- .and_then(Path::parent)
296
- .expect("<root>/crates/feltdb")
297
- }
298
-
299
- /// Whether this machine can compile to WebAssembly. The libdir's existence is
300
- /// the fact worth having: `rustc` reports every known target as installed.
301
- fn wasm_target_installed() -> bool {
302
- Command::new("rustc")
303
- .args([
304
- "--print",
305
- "target-libdir",
306
- "--target",
307
- "wasm32-unknown-unknown",
308
- ])
309
- .output()
310
- .ok()
311
- .filter(|o| o.status.success())
312
- .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
313
- .is_some_and(|libdir| Path::new(&libdir).is_dir())
314
- }
315
-
316
- fn build_module(manifest_dir: &Path, target_dir: Option<&Path>) -> Vec<u8> {
317
- let mut command = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()));
318
- command
319
- .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
320
- .current_dir(manifest_dir)
321
- .env_remove("CARGO_TARGET_DIR")
322
- .env_remove("RUSTFLAGS");
323
- if let Some(dir) = target_dir {
324
- command.env("CARGO_TARGET_DIR", dir);
325
- }
326
- let output = command.output().expect("cargo is runnable");
327
- assert!(
328
- output.status.success(),
329
- "building the WASM contract at {} failed:\n{}",
330
- manifest_dir.display(),
331
- String::from_utf8_lossy(&output.stderr)
332
- );
333
- let built = match target_dir {
334
- Some(dir) => {
335
- dir.join("wasm32-unknown-unknown/release/feltdb_agent_transition_contract_wasm.wasm")
336
- }
337
- None => manifest_dir.join(WASM_ARTIFACT),
338
- };
339
- std::fs::read(built).expect("the build emits one module")
340
- }
341
-
342
- fn contract_module() -> Option<&'static [u8]> {
343
- static MODULE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
344
- MODULE
345
- .get_or_init(|| {
346
- if let Ok(path) = std::env::var("FELTDB_AGENT_TRANSITION_CONTRACT_WASM") {
347
- return Some(
348
- std::fs::read(path).expect("FELTDB_AGENT_TRANSITION_CONTRACT_WASM is readable"),
349
- );
350
- }
351
- wasm_target_installed().then(|| build_module(&repository_root().join(WASM_CRATE), None))
352
- })
353
- .as_deref()
354
- }
355
-
356
- fn skipped(test: &str) {
357
- eprintln!(
358
- "SKIPPED {test}: the wasm32-unknown-unknown target is not installed, so the WASM contract \
359
- could not be built. Native conformance against the committed expectations still ran."
360
- );
361
- }
362
-
363
- struct WasmContract {
364
- store: wasmi::Store<()>,
365
- memory: wasmi::Memory,
366
- alloc: wasmi::TypedFunc<u32, u32>,
367
- evaluate: wasmi::TypedFunc<(u32, u32), u64>,
368
- free: wasmi::TypedFunc<(u32, u32), ()>,
369
- }
370
-
371
- impl WasmContract {
372
- fn instantiate(bytes: &[u8]) -> Self {
373
- let engine = wasmi::Engine::default();
374
- let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
375
- let mut store = wasmi::Store::new(&engine, ());
376
- let instance = wasmi::Linker::<()>::new(&engine)
377
- .instantiate_and_start(&mut store, &module)
378
- .expect("an empty linker satisfies a module with no imports");
379
- Self {
380
- memory: instance.get_memory(&store, "memory").expect("memory"),
381
- alloc: instance
382
- .get_typed_func(&store, "contract_alloc")
383
- .expect("contract_alloc"),
384
- evaluate: instance
385
- .get_typed_func(&store, "contract_evaluate")
386
- .expect("contract_evaluate"),
387
- free: instance
388
- .get_typed_func(&store, "contract_free")
389
- .expect("contract_free"),
390
- store,
391
- }
392
- }
393
-
394
- fn evaluate(&mut self, input: &[u8]) -> Vec<u8> {
395
- let len = input.len() as u32;
396
- let ptr = self.alloc.call(&mut self.store, len).expect("allocation");
397
- self.memory
398
- .write(&mut self.store, ptr as usize, input)
399
- .expect("input fits");
400
- let packed = self
401
- .evaluate
402
- .call(&mut self.store, (ptr, len))
403
- .expect("the contract never traps");
404
- let (out_ptr, out_len) = ((packed >> 32) as usize, (packed & 0xffff_ffff) as usize);
405
- let mut output = vec![0u8; out_len];
406
- self.memory
407
- .read(&self.store, out_ptr, &mut output)
408
- .expect("output in memory");
409
- self.free.call(&mut self.store, (ptr, len)).expect("free");
410
- self.free
411
- .call(&mut self.store, (out_ptr as u32, out_len as u32))
412
- .expect("free");
413
- output
414
- }
415
- }
416
-
417
- fn wasm_or_skip(test: &str) -> Option<WasmContract> {
418
- match contract_module() {
419
- Some(bytes) => Some(WasmContract::instantiate(bytes)),
420
- None => {
421
- skipped(test);
422
- None
423
- }
424
- }
425
- }
426
-
427
- // ---------------------------------------------------------------------------
428
- // Conformance
429
- // ---------------------------------------------------------------------------
430
-
431
- #[test]
432
- fn corpus_addresses_this_contract_at_this_version() {
433
- let corpus = corpus();
434
- assert_eq!(corpus["corpus_version"], 1);
435
- assert_eq!(corpus["contract"], AGENT_TRANSITION_CONTRACT_ID);
436
- assert_eq!(
437
- corpus["contract_version"],
438
- AGENT_TRANSITION_CONTRACT_VERSION
439
- );
440
- assert!(
441
- cases().len() >= 300,
442
- "the corpus is the proof; it should not shrink silently"
443
- );
444
- assert!(sequences().len() >= 12);
445
- let mut names: Vec<&str> = cases().iter().map(|c| text(c, "name")).collect();
446
- names.sort_unstable();
447
- let total = names.len();
448
- names.dedup();
449
- assert_eq!(total, names.len(), "case names identify cases");
450
- }
451
-
452
- #[test]
453
- fn native_matches_the_committed_expectations() {
454
- let mut divergent = Vec::new();
455
- for case in cases() {
456
- let produced = String::from_utf8(evaluate_canonical(&case_bytes(case))).unwrap();
457
- if produced != text(case, "expected_output") {
458
- divergent.push(format!(
459
- "{}\n expected: {}\n native: {}",
460
- text(case, "name"),
461
- text(case, "expected_output"),
462
- produced
463
- ));
464
- }
465
- }
466
- assert!(
467
- divergent.is_empty(),
468
- "{} divergent:\n{}",
469
- divergent.len(),
470
- divergent.join("\n")
471
- );
472
- }
473
-
474
- #[test]
475
- fn native_sequences_match_the_committed_expectations() {
476
- run_sequences(
477
- evaluate_canonical,
478
- |step, committed_input, derived_input, _| {
479
- assert_eq!(
480
- derived_input, committed_input,
481
- "{step}: the harness derived different records than were committed"
482
- );
483
- },
484
- )
485
- .unwrap();
486
- }
487
-
488
- #[test]
489
- fn wasm_matches_the_committed_expectations() {
490
- let Some(mut wasm) = wasm_or_skip("wasm_matches_the_committed_expectations") else {
491
- return;
492
- };
493
- let mut divergent = Vec::new();
494
- for case in cases() {
495
- let produced = String::from_utf8(wasm.evaluate(&case_bytes(case))).unwrap();
496
- if produced != text(case, "expected_output") {
497
- divergent.push(format!(
498
- "{}\n expected: {}\n wasm: {}",
499
- text(case, "name"),
500
- text(case, "expected_output"),
501
- produced
502
- ));
503
- }
504
- }
505
- assert!(
506
- divergent.is_empty(),
507
- "{} divergent:\n{}",
508
- divergent.len(),
509
- divergent.join("\n")
510
- );
511
- run_sequences(|input| wasm.evaluate(input), |_, _, _, _| {}).unwrap();
512
- }
513
-
514
- #[test]
515
- fn native_and_wasm_agree_byte_for_byte() {
516
- let Some(mut wasm) = wasm_or_skip("native_and_wasm_agree_byte_for_byte") else {
517
- return;
518
- };
519
- for case in cases() {
520
- let bytes = case_bytes(case);
521
- assert_eq!(
522
- evaluate_canonical(&bytes),
523
- wasm.evaluate(&bytes),
524
- "{}",
525
- text(case, "name")
526
- );
527
- }
528
- }
529
-
530
- /// Every output is canonical: parsing it and writing it back changes nothing.
531
- #[test]
532
- fn every_committed_output_is_canonical() {
533
- let mut all: Vec<&str> = cases().iter().map(|c| text(c, "expected_output")).collect();
534
- for sequence in sequences() {
535
- all.extend(
536
- sequence["steps"]
537
- .as_array()
538
- .unwrap()
539
- .iter()
540
- .map(|s| text(s, "expected_output")),
541
- );
542
- }
543
- for output in all {
544
- let value: Value = serde_json::from_str(output).unwrap();
545
- assert_eq!(serde_json::to_string(&value).unwrap(), output);
546
- assert_eq!(value["contract"], AGENT_TRANSITION_CONTRACT_ID);
547
- assert_eq!(value["version"], AGENT_TRANSITION_CONTRACT_VERSION);
548
- assert!(
549
- value.get("error").is_some() != value.get("outcome").is_some(),
550
- "exactly one of error/outcome"
551
- );
552
- }
553
- }
554
-
555
- /// The corpus exercises every operation and every refusal code. A guard against
556
- /// the corpus losing a dimension, not a substitute for reading it.
557
- #[test]
558
- fn corpus_covers_every_operation_and_every_error_code() {
559
- let mut codes = std::collections::BTreeSet::new();
560
- let mut committed_ops = std::collections::BTreeSet::new();
561
- let mut outputs: Vec<(Option<String>, String)> = Vec::new();
562
- for case in cases() {
563
- let op = case
564
- .get("input")
565
- .and_then(|i| i["operation"]["op"].as_str())
566
- .map(str::to_string);
567
- outputs.push((op, text(case, "expected_output").to_string()));
568
- }
569
- for sequence in sequences() {
570
- for step in sequence["steps"].as_array().unwrap() {
571
- outputs.push((
572
- Some(text(&step["operation"], "op").to_string()),
573
- text(step, "expected_output").to_string(),
574
- ));
575
- }
576
- }
577
- for (op, output) in outputs {
578
- let value: Value = serde_json::from_str(&output).unwrap();
579
- if let Some(code) = value["error"]["code"].as_str() {
580
- codes.insert(code.to_string());
581
- } else if value["outcome"]["kind"] == "commit" {
582
- committed_ops.extend(op);
583
- }
584
- }
585
- for code in [
586
- "CONTRACT_INPUT_INVALID",
587
- "CONTRACT_ID_MISMATCH",
588
- "CONTRACT_VERSION_UNSUPPORTED",
589
- "NOT_FOUND",
590
- "ALREADY_EXISTS",
591
- "INVALID_TRANSITION",
592
- "TERMINAL",
593
- "FENCE_STALE",
594
- "OWNERSHIP_CONFLICT",
595
- "LEASE_MISMATCH",
596
- "LEASE_EXPIRED",
597
- "LEASE_HELD",
598
- "VERSION_CONFLICT",
599
- "EVIDENCE_INVALID",
600
- "EVIDENCE_REQUIRED",
601
- "OUTCOME_UNCERTAIN",
602
- "INTENT_UNKNOWN",
603
- "INTENT_RESOLVED",
604
- "HANDOFF_CLAIMED",
605
- "HANDOFF_STALE",
606
- ] {
607
- assert!(codes.contains(code), "no corpus entry produces {code}");
608
- }
609
- for op in [
610
- "create_task",
611
- "acquire",
612
- "renew",
613
- "checkpoint",
614
- "record_intent",
615
- "record_outcome",
616
- "reconcile",
617
- "create_handoff",
618
- "claim_handoff",
619
- "accept_handoff",
620
- ] {
621
- assert!(committed_ops.contains(op), "no corpus entry commits {op}");
622
- }
623
- }
624
-
625
- /// Properties that must hold of every decision in the corpus, whatever the
626
- /// individual expectation says. These are the contract's invariants, stated
627
- /// independently of any one case.
628
- #[test]
629
- fn invariants_hold_for_every_decision() {
630
- let mut outputs: Vec<(Value, Value)> = Vec::new();
631
- for case in cases() {
632
- if let Some(input) = case.get("input") {
633
- outputs.push((
634
- input.clone(),
635
- serde_json::from_str(text(case, "expected_output")).unwrap(),
636
- ));
637
- }
638
- }
639
- run_sequences(evaluate_canonical, |_, _, derived, produced| {
640
- outputs.push((
641
- serde_json::from_str(derived).unwrap(),
642
- serde_json::from_str(produced).unwrap(),
643
- ));
644
- })
645
- .unwrap();
646
- for (input, output) in outputs {
647
- let outcome = &output["outcome"];
648
- if outcome["kind"] != "commit" {
649
- continue;
650
- }
651
- let op = text(&input["operation"], "op");
652
- let now = input["now_ms"].as_u64().unwrap();
653
- let writes = outcome["writes"].as_array().unwrap();
654
- for write in writes {
655
- // Every write is conditional: there is no blind overwrite.
656
- assert!(
657
- write["expect"].is_object() && !write["expect"].as_object().unwrap().is_empty()
658
- );
659
- // A stored citation is `verified` only if it names the version FeltDB observed.
660
- let value = &write["value"];
661
- for citation in value["evidence"].as_array().into_iter().flatten() {
662
- if citation["verified"] == true {
663
- assert_eq!(citation["kind"], "record");
664
- assert!(
665
- citation["version"].as_u64().is_some(),
666
- "a verified citation pins a version"
667
- );
668
- assert!(
669
- outcome["guards"]
670
- .as_array()
671
- .unwrap()
672
- .iter()
673
- .any(|g| g["collection"] == citation["collection"]
674
- && g["id"] == citation["id"]
675
- && g["version"] == citation["version"]),
676
- "a verified citation must be guarded: {citation}"
677
- );
678
- } else {
679
- assert_eq!(
680
- citation["kind"], "external",
681
- "only external citations are unverified"
682
- );
683
- }
684
- }
685
- // A lease written by a commit is live at the moment of the decision, and a
686
- // newly granted or renewed one is at most the cap long.
687
- if let Some(expires) = value["lease"]["expiresAt"].as_u64() {
688
- assert!(
689
- expires > now,
690
- "a commit never writes an already-expired lease"
691
- );
692
- if matches!(op, "create_task" | "acquire" | "renew" | "claim_handoff") {
693
- assert!(expires - now <= 86_400_000);
694
- }
695
- }
696
- }
697
- // A completion cites at least one verified record, and no unresolved action remains.
698
- let completing = writes.iter().find(|w| {
699
- w["collection"] == "_feltdb.AgentTask" && w["value"]["status"] == "completed"
700
- });
701
- if let Some(task) = completing {
702
- assert_eq!(op, "checkpoint");
703
- assert_eq!(
704
- task["value"]["unresolved_intents"], 0,
705
- "completed with an unresolved action"
706
- );
707
- assert!(
708
- task["value"]["lease"].is_null(),
709
- "a terminal task holds no lease"
710
- );
711
- let checkpoint = writes
712
- .iter()
713
- .find(|w| w["collection"] == "_feltdb.AgentCheckpoint")
714
- .unwrap();
715
- assert!(
716
- checkpoint["value"]["evidence"]
717
- .as_array()
718
- .unwrap()
719
- .iter()
720
- .any(|c| c["kind"] == "record" && c["verified"] == true),
721
- "a completion without a verified record citation"
722
- );
723
- }
724
- // Owner-bearing writes fence on the lease they relied on.
725
- if matches!(
726
- op,
727
- "renew" | "checkpoint" | "record_intent" | "record_outcome" | "reconcile"
728
- ) {
729
- let task = writes
730
- .iter()
731
- .find(|w| w["collection"] == "_feltdb.AgentTask")
732
- .unwrap();
733
- assert!(
734
- task["expect"]["lease_id"].is_string(),
735
- "{op} must fence on the lease"
736
- );
737
- assert!(task["expect"]["version"].is_u64());
738
- }
739
- // A takeover changes the fence by exactly one.
740
- if matches!(op, "acquire" | "claim_handoff") {
741
- let before = input["records"]["task"]["value"]["fence"].as_u64().unwrap();
742
- let task = writes
743
- .iter()
744
- .find(|w| w["collection"] == "_feltdb.AgentTask")
745
- .unwrap();
746
- assert_eq!(task["value"]["fence"].as_u64().unwrap(), before + 1);
747
- }
748
- }
749
- }
750
-
751
- #[test]
752
- fn evaluation_is_deterministic_across_repeats() {
753
- for case in cases().iter().step_by(7) {
754
- let bytes = case_bytes(case);
755
- let first = evaluate_canonical(&bytes);
756
- for _ in 0..8 {
757
- assert_eq!(evaluate_canonical(&bytes), first);
758
- }
759
- }
760
- }
761
-
762
- #[test]
763
- fn wasm_is_deterministic_across_fresh_instances() {
764
- let Some(bytes) = contract_module() else {
765
- return skipped("wasm_is_deterministic_across_fresh_instances");
766
- };
767
- let mut a = WasmContract::instantiate(bytes);
768
- for case in cases().iter().step_by(11) {
769
- let input = case_bytes(case);
770
- let first = a.evaluate(&input);
771
- assert_eq!(
772
- WasmContract::instantiate(bytes).evaluate(&input),
773
- first,
774
- "{}",
775
- text(case, "name")
776
- );
777
- assert_eq!(a.evaluate(&input), first);
778
- }
779
- }
780
-
781
- /// Member order and whitespace in the input are not semantics.
782
- #[test]
783
- fn canonical_and_pretty_encodings_decide_identically() {
784
- for case in cases()
785
- .iter()
786
- .filter(|c| c.get("input").is_some())
787
- .step_by(5)
788
- {
789
- let input = &case["input"];
790
- let pretty = serde_json::to_vec_pretty(input).unwrap();
791
- assert_eq!(
792
- evaluate_canonical(&pretty),
793
- evaluate_canonical(&case_bytes(case)),
794
- "{}",
795
- text(case, "name")
796
- );
797
- }
798
- }
799
-
800
- // ---------------------------------------------------------------------------
801
- // The WASM module's capabilities
802
- // ---------------------------------------------------------------------------
803
-
804
- #[test]
805
- fn wasm_module_declares_no_imports_and_exports_only_the_abi() {
806
- let Some(bytes) = contract_module() else {
807
- return skipped("wasm_module_declares_no_imports");
808
- };
809
- let engine = wasmi::Engine::default();
810
- let module = wasmi::Module::new(&engine, bytes).unwrap();
811
- let imports: Vec<String> = module
812
- .imports()
813
- .map(|i| format!("{}::{}", i.module(), i.name()))
814
- .collect();
815
- assert!(
816
- imports.is_empty(),
817
- "the module must not import anything: {imports:?}"
818
- );
819
- // Older toolchains (CI builds with 1.96) make the linker export two address globals, the end of
820
- // static data and the start of the heap. They are numbers, not behavior; newer toolchains omit
821
- // them. Tolerate exactly those two names, and only as globals: anything else, or either of them
822
- // as a function, is a new capability and fails.
823
- const LINKER_ADDRESS_GLOBALS: [&str; 2] = ["__data_end", "__heap_base"];
824
- let mut exports: Vec<String> = Vec::new();
825
- for export in module.exports() {
826
- let name = export.name();
827
- if LINKER_ADDRESS_GLOBALS.contains(&name) {
828
- assert!(
829
- export.ty().global().is_some(),
830
- "{name} may only be exported as a global"
831
- );
832
- continue;
833
- }
834
- exports.push(name.to_string());
835
- }
836
- exports.sort();
837
- assert_eq!(
838
- exports,
839
- [
840
- "contract_alloc",
841
- "contract_evaluate",
842
- "contract_free",
843
- "contract_version",
844
- "memory"
845
- ]
846
- );
847
- let mut store = wasmi::Store::new(&engine, ());
848
- let instance = wasmi::Linker::<()>::new(&engine)
849
- .instantiate_and_start(&mut store, &module)
850
- .unwrap();
851
- let version = instance
852
- .get_typed_func::<(), u32>(&store, "contract_version")
853
- .unwrap()
854
- .call(&mut store, ())
855
- .unwrap();
856
- assert_eq!(version, AGENT_TRANSITION_CONTRACT_VERSION);
857
- for forbidden in [
858
- "clock_time_get",
859
- "random_get",
860
- "fd_read",
861
- "fd_write",
862
- "sock_recv",
863
- "path_open",
864
- "wasi_snapshot",
865
- ] {
866
- assert!(
867
- !bytes
868
- .windows(forbidden.len())
869
- .any(|w| w == forbidden.as_bytes()),
870
- "WASI symbol {forbidden}"
871
- );
872
- }
873
- }
874
-
875
- #[test]
876
- fn wasm_never_traps_on_hostile_input() {
877
- let Some(mut wasm) = wasm_or_skip("wasm_never_traps_on_hostile_input") else {
878
- return;
879
- };
880
- let deep = format!("{}{}", "[".repeat(5000), "]".repeat(5000));
881
- let long_string = format!("{{\"contract\":\"{}\"}}", "x".repeat(200_000));
882
- let hostile: Vec<Vec<u8>> = vec![
883
- vec![],
884
- b"\xff\xfe".to_vec(),
885
- b"{".to_vec(),
886
- deep.into_bytes(),
887
- long_string.into_bytes(),
888
- b"{\"contract\":\"\\ud800\"}".to_vec(),
889
- vec![0u8; 4096],
890
- ];
891
- for input in hostile {
892
- let out = wasm.evaluate(&input);
893
- assert_eq!(
894
- String::from_utf8(out).unwrap(),
895
- String::from_utf8(evaluate_canonical(&input)).unwrap()
896
- );
897
- }
898
- }
899
-
900
- // ---------------------------------------------------------------------------
901
- // Mutation: a corpus that has never failed proves nothing
902
- // ---------------------------------------------------------------------------
903
-
904
- /// Seeded defects in a copy of the real WASM source. Each must be caught by at
905
- /// least one committed expectation. The anchors must occur exactly once, so a
906
- /// refactor that moves them fails here instead of silently disarming the test.
907
- const MUTATIONS: &[(&str, &str, &str)] = &[
908
- ("lease expiry boundary is exclusive of now == expiresAt", "if now >= lease.expires_at {", "if now > lease.expires_at {"),
909
- ("an unexpired lease may be taken at its last millisecond", "if now < held.expires_at {\n return Err(Code::LeaseHeld);", "if now + 1 < held.expires_at {\n return Err(Code::LeaseHeld);"),
910
- ("the fence is not checked", "if fence != t.fence {", "if false {"),
911
- ("ownership is not checked", "if actor != t.owner {", "if false {"),
912
- ("completion needs no evidence", "if status == \"completed\" {\n if c.verified == 0 {", "if status == \"completed\" {\n if false {"),
913
- ("completion ignores unresolved actions", "if t.unresolved > 0 {", "if false {"),
914
- ("reconciliation needs no evidence", " if c.verified == 0 {\n return Err(Code::EvidenceRequired);\n }\n n.conclusion", " n.conclusion"),
915
- ("an unverified outcome resolves the action", "resolves = c.verified > 0;", "resolves = true;"),
916
- ("an agent record may be cited as evidence", "if c.starts_with(AGENT_PREFIX) {", "if false {"),
917
- ("a recorded intent may be recorded again", "if let Some(existing) = &rec.intent {", "if let (Some(existing), true) = (&rec.intent, false) {"),
918
- ("ready may complete directly", "(\"ready\", \"cancelled\")", "(\"ready\", \"completed\") | (\"ready\", \"cancelled\")"),
919
- ("a retried claim must match the lease", "&& held.id == new_id;", "&& true;"),
920
- ("cited evidence is not guarded", "guards.push((c, i, s.version));", "guards.push((c, i, s.version)); guards.clear();"),
921
- ("a terminal task keeps its lease", "if is_terminal(&status) {\n n.lease = None;", "if false {\n n.lease = None;"),
922
- ("a stale handoff is claimable", "} else if t.owner != h.from", "} else if false && t.owner != h.from"),
923
- ];
924
-
925
- #[test]
926
- fn every_seeded_defect_is_caught_by_the_corpus() {
927
- if !wasm_target_installed() {
928
- return skipped("every_seeded_defect_is_caught_by_the_corpus");
929
- }
930
- let source = repository_root().join(WASM_CRATE);
931
- let decide = std::fs::read_to_string(source.join("src/decide.rs")).unwrap();
932
- let scratch =
933
- std::env::temp_dir().join(format!("agent-transition-mutants-{}", std::process::id()));
934
- let _ = std::fs::remove_dir_all(&scratch);
935
- // Anchors are text in a formatted file. Check them all before building any,
936
- // so a reformat that moves several is reported once, not one build at a time.
937
- let unanchored: Vec<&str> = MUTATIONS
938
- .iter()
939
- .filter(|(_, anchor, _)| decide.matches(anchor).count() != 1)
940
- .map(|(name, _, _)| *name)
941
- .collect();
942
- assert!(
943
- unanchored.is_empty(),
944
- "mutation anchors that no longer occur exactly once: {unanchored:?}"
945
- );
946
- let mut missed = Vec::new();
947
- let mut report = Vec::new();
948
- for (index, (name, anchor, replacement)) in MUTATIONS.iter().enumerate() {
949
- let dir = scratch.join(format!("m{index}"));
950
- std::fs::create_dir_all(dir.join("src")).unwrap();
951
- for file in ["Cargo.toml", "src/lib.rs", "src/json.rs"] {
952
- std::fs::copy(source.join(file), dir.join(file)).unwrap();
953
- }
954
- std::fs::write(
955
- dir.join("src/decide.rs"),
956
- decide.replacen(anchor, replacement, 1),
957
- )
958
- .unwrap();
959
- let module = build_module(&dir, Some(&scratch.join("target")));
960
- let mut wasm = WasmContract::instantiate(&module);
961
- let mut caught = 0;
962
- for case in cases() {
963
- if String::from_utf8(wasm.evaluate(&case_bytes(case))).unwrap()
964
- != text(case, "expected_output")
965
- {
966
- caught += 1;
967
- }
968
- }
969
- let sequence_caught = run_sequences(|input| wasm.evaluate(input), |_, _, _, _| {}).is_err();
970
- if caught == 0 && !sequence_caught {
971
- missed.push(*name);
972
- }
973
- report.push(format!(
974
- "{caught:>4} cases{} | {name}",
975
- if sequence_caught { " + a sequence" } else { "" }
976
- ));
977
- }
978
- let _ = std::fs::remove_dir_all(&scratch);
979
- eprintln!("mutation results:\n{}", report.join("\n"));
980
- assert!(
981
- missed.is_empty(),
982
- "defects the corpus did not catch: {missed:?}"
983
- );
984
- }
985
-
986
- // ---------------------------------------------------------------------------
987
- // Blessing
988
- // ---------------------------------------------------------------------------
989
-
990
- #[test]
991
- #[ignore = "re-records the committed corpus; run deliberately"]
992
- fn bless() {
993
- assert_eq!(
994
- std::env::var("FELTDB_AGENT_TRANSITION_BLESS").as_deref(),
995
- Ok("1"),
996
- "set FELTDB_AGENT_TRANSITION_BLESS=1"
997
- );
998
- let module = contract_module().expect(
999
- "blessing needs the WASM implementation: refusing to record from one implementation alone",
1000
- );
1001
- let mut wasm = WasmContract::instantiate(module);
1002
- let mut recorded_cases = Vec::new();
1003
- for mut case in build::cases() {
1004
- let bytes = case_bytes(&case);
1005
- let native = evaluate_canonical(&bytes);
1006
- assert_eq!(
1007
- native,
1008
- wasm.evaluate(&bytes),
1009
- "implementations disagree on {}",
1010
- text(&case, "name")
1011
- );
1012
- // Typed cases carry their canonical bytes, so a consumer that cannot
1013
- // reproduce them (JavaScript has no u64) presents exactly the same input.
1014
- if case.get("input").is_some() {
1015
- case["canonical_input"] = Value::String(String::from_utf8(bytes).unwrap());
1016
- }
1017
- case["expected_output"] = Value::String(String::from_utf8(native).unwrap());
1018
- recorded_cases.push(case);
1019
- }
1020
- let mut recorded_sequences = Vec::new();
1021
- for mut sequence in build::sequences() {
1022
- let mut world = World::default();
1023
- for step in sequence["steps"].as_array_mut().unwrap() {
1024
- for seed in step["seed"].as_array().into_iter().flatten() {
1025
- world.seed(text(seed, "collection"), text(seed, "id"));
1026
- }
1027
- let input = serde_json::to_vec(&step_input(&world, step)).unwrap();
1028
- let native = evaluate_canonical(&input);
1029
- assert_eq!(
1030
- native,
1031
- wasm.evaluate(&input),
1032
- "implementations disagree on {}",
1033
- text(step, "label")
1034
- );
1035
- step["canonical_input"] = Value::String(String::from_utf8(input).unwrap());
1036
- step["expected_output"] = Value::String(String::from_utf8(native.clone()).unwrap());
1037
- world
1038
- .apply(
1039
- step["now_ms"].as_u64().unwrap(),
1040
- &serde_json::from_slice(&native).unwrap(),
1041
- )
1042
- .unwrap();
1043
- }
1044
- recorded_sequences.push(sequence);
1045
- }
1046
- let corpus = json!({
1047
- "corpus_version": 1,
1048
- "contract": AGENT_TRANSITION_CONTRACT_ID,
1049
- "contract_version": AGENT_TRANSITION_CONTRACT_VERSION,
1050
- "about": "Committed conformance corpus for feltdb.agent.transition v1. Cases are single decisions; \
1051
- sequences are ordered steps whose records the harness derives from a state model. \
1052
- Regenerate only with the bless test in agent_transition_contract_conformance.rs.",
1053
- "cases": recorded_cases,
1054
- "sequences": recorded_sequences,
1055
- });
1056
- let mut text = serde_json::to_string_pretty(&corpus).unwrap();
1057
- text.push('\n');
1058
- std::fs::write(corpus_path(), text).unwrap();
1059
- }
1060
-
1061
- /// The corpus builder. Used only by `bless`; the committed JSON is the artifact.
1062
- mod build {
1063
- use super::*;
1064
-
1065
- pub fn input(now: u64, operation: Value, records: Value) -> Value {
1066
- json!({
1067
- "contract": AGENT_TRANSITION_CONTRACT_ID, "now_ms": now, "operation": operation,
1068
- "records": records, "version": AGENT_TRANSITION_CONTRACT_VERSION,
1069
- })
1070
- }
1071
-
1072
- fn stored(value: Value, version: u64) -> Value {
1073
- json!({"value": value, "version": version})
1074
- }
1075
-
1076
- pub fn task(f: impl FnOnce(&mut Value)) -> Value {
1077
- let mut value = json!({
1078
- "checkpoint_version": 3, "created_at_ms": 1000, "fence": 2,
1079
- "lease": {"expiresAt": 10000, "leaseId": "L2"}, "objective": "Ship it",
1080
- "owner": "agent-a", "status": "running", "task_id": "t1",
1081
- "unresolved_intents": 0, "updated_at_ms": 2000,
1082
- });
1083
- f(&mut value);
1084
- stored(value, 7)
1085
- }
1086
-
1087
- fn terminal_task(status: &str) -> Value {
1088
- task(|t| {
1089
- t["status"] = json!(status);
1090
- t["lease"] = Value::Null;
1091
- })
1092
- }
1093
-
1094
- fn handoff(f: impl FnOnce(&mut Value)) -> Value {
1095
- let mut value = json!({
1096
- "checkpoint_version": 3, "from": "agent-a", "from_fence": 2,
1097
- "handoff_id": "h1", "status": "open", "task_id": "t1", "to": "agent-b",
1098
- });
1099
- f(&mut value);
1100
- stored(value, 4)
1101
- }
1102
-
1103
- fn claimed_handoff(claimant: &str, fence: u64) -> Value {
1104
- handoff(|h| {
1105
- h["status"] = json!("claimed");
1106
- h["claimant"] = json!(claimant);
1107
- h["claimed_fence"] = json!(fence);
1108
- })
1109
- }
1110
-
1111
- fn intent(state: &str) -> Value {
1112
- let mut value = json!({
1113
- "action": "send-email", "evidence": [], "intended_at_ms": 3000, "intended_fence": 2,
1114
- "key": "k1", "state": state, "task_id": "t1", "verified": false,
1115
- });
1116
- if state != "intended" {
1117
- value["result"] = json!("succeeded");
1118
- }
1119
- if state == "resolved" {
1120
- value["verified"] = json!(true);
1121
- value["evidence"] = json!([{"collection": "receipts", "id": "r0", "kind": "record", "verified": true, "version": 1}]);
1122
- }
1123
- stored(value, 3)
1124
- }
1125
-
1126
- fn with_unresolved(n: u64) -> Value {
1127
- task(|t| t["unresolved_intents"] = json!(n))
1128
- }
1129
-
1130
- fn rec(collection: &str, id: &str) -> Value {
1131
- json!({"collection": collection, "id": id, "kind": "record"})
1132
- }
1133
-
1134
- fn ext(uri: &str) -> Value {
1135
- json!({"kind": "external", "uri": uri})
1136
- }
1137
-
1138
- fn seen(collection: &str, id: &str, version: u64) -> Value {
1139
- json!({"collection": collection, "id": id, "version": version})
1140
- }
1141
-
1142
- fn base_op(op: &str) -> Value {
1143
- match op {
1144
- "create_task" => {
1145
- json!({"op": "create_task", "task_id": "t1", "actor": "agent-a", "objective": "Ship it", "lease_id": "L1", "lease_ms": 60000})
1146
- }
1147
- "acquire" => {
1148
- json!({"op": "acquire", "task_id": "t1", "actor": "agent-b", "lease_id": "L3", "lease_ms": 60000})
1149
- }
1150
- "renew" => {
1151
- json!({"op": "renew", "task_id": "t1", "actor": "agent-a", "fence": 2, "lease_id": "L2", "lease_ms": 60000})
1152
- }
1153
- "checkpoint" => {
1154
- json!({"op": "checkpoint", "task_id": "t1", "actor": "agent-a", "fence": 2, "lease_id": "L2", "expected_version": 3, "summary": "Halfway.", "evidence": []})
1155
- }
1156
- "record_intent" => {
1157
- json!({"op": "record_intent", "task_id": "t1", "actor": "agent-a", "fence": 2, "lease_id": "L2", "idempotency_key": "k1", "action": "send-email"})
1158
- }
1159
- "record_outcome" => {
1160
- json!({"op": "record_outcome", "task_id": "t1", "actor": "agent-a", "fence": 2, "lease_id": "L2", "idempotency_key": "k1", "result": "succeeded", "evidence": [rec("receipts", "r1")]})
1161
- }
1162
- "reconcile" => {
1163
- json!({"op": "reconcile", "task_id": "t1", "actor": "agent-a", "fence": 2, "lease_id": "L2", "idempotency_key": "k1", "conclusion": "executed", "evidence": [rec("receipts", "r1")]})
1164
- }
1165
- "create_handoff" => {
1166
- json!({"op": "create_handoff", "task_id": "t1", "handoff_id": "h1", "actor": "agent-a", "fence": 2, "lease_id": "L2", "to": "agent-b"})
1167
- }
1168
- "claim_handoff" => {
1169
- json!({"op": "claim_handoff", "handoff_id": "h1", "claimant": "agent-b", "lease_id": "L3", "lease_ms": 60000})
1170
- }
1171
- "accept_handoff" => {
1172
- json!({"op": "accept_handoff", "handoff_id": "h1", "claimant": "agent-b", "fence": 3, "lease_id": "L3"})
1173
- }
1174
- other => panic!("{other}"),
1175
- }
1176
- }
1177
-
1178
- fn with(mut op: Value, changes: Value) -> Value {
1179
- for (key, value) in changes.as_object().unwrap() {
1180
- if value.is_null() {
1181
- op.as_object_mut().unwrap().remove(key);
1182
- } else {
1183
- op[key] = value.clone();
1184
- }
1185
- }
1186
- op
1187
- }
1188
-
1189
- /// The valid base input for each operation, with the records it needs.
1190
- fn base(op: &str) -> (u64, Value, Value) {
1191
- let operation = base_op(op);
1192
- let receipts = json!([seen("receipts", "r1", 1)]);
1193
- let (now, records) = match op {
1194
- "create_task" => (5000, json!({})),
1195
- "acquire" => (12000, json!({"task": task(|_| {})})),
1196
- "renew" => (5000, json!({"task": task(|_| {})})),
1197
- "checkpoint" => (5000, json!({"task": task(|_| {}), "evidence": []})),
1198
- "record_intent" => (5000, json!({"task": task(|_| {})})),
1199
- "record_outcome" | "reconcile" => (
1200
- 5000,
1201
- json!({"task": with_unresolved(1), "intent": intent("intended"), "evidence": receipts}),
1202
- ),
1203
- "create_handoff" => (5000, json!({"task": task(|_| {})})),
1204
- "claim_handoff" => (
1205
- 5000,
1206
- json!({"task": task(|_| {}), "handoff": handoff(|_| {})}),
1207
- ),
1208
- "accept_handoff" => (
1209
- 5000,
1210
- json!({
1211
- "task": task(|t| { t["fence"] = json!(3); t["owner"] = json!("agent-b"); t["lease"] = json!({"expiresAt": 65000, "leaseId": "L3"}); }),
1212
- "handoff": claimed_handoff("agent-b", 3),
1213
- }),
1214
- ),
1215
- other => panic!("{other}"),
1216
- };
1217
- (now, operation, records)
1218
- }
1219
-
1220
- const OPS: [&str; 10] = [
1221
- "create_task",
1222
- "acquire",
1223
- "renew",
1224
- "checkpoint",
1225
- "record_intent",
1226
- "record_outcome",
1227
- "reconcile",
1228
- "create_handoff",
1229
- "claim_handoff",
1230
- "accept_handoff",
1231
- ];
1232
-
1233
- pub fn cases() -> Vec<Value> {
1234
- let mut out: Vec<Value> = Vec::new();
1235
- let mut add = |name: String, why: &str, input: Value| {
1236
- out.push(json!({"name": name, "why": why, "input": input}));
1237
- };
1238
-
1239
- // Every operation's valid base input.
1240
- for op in OPS {
1241
- let (now, operation, records) = base(op);
1242
- add(
1243
- format!("{op}/valid"),
1244
- "the base input of the operation commits",
1245
- input(now, operation, records),
1246
- );
1247
- }
1248
-
1249
- // Structural mutations of every base input: the shape is part of the contract.
1250
- for op in OPS {
1251
- let (now, operation, records) = base(op);
1252
- let members: Vec<String> = operation.as_object().unwrap().keys().cloned().collect();
1253
- for member in members.iter().filter(|m| *m != "op") {
1254
- for (variant, replacement) in [
1255
- ("removed", None),
1256
- ("null", Some(Value::Null)),
1257
- ("array", Some(json!([]))),
1258
- ("number_float", Some(json!(1.5))),
1259
- ] {
1260
- let mut mutated = operation.clone();
1261
- match replacement {
1262
- None => {
1263
- mutated.as_object_mut().unwrap().remove(member);
1264
- }
1265
- Some(v) => mutated[member] = v,
1266
- }
1267
- add(
1268
- format!("{op}/member/{member}/{variant}"),
1269
- "an operation carries exactly its defined members, typed",
1270
- input(now, mutated, records.clone()),
1271
- );
1272
- }
1273
- }
1274
- let mut extra = operation.clone();
1275
- extra["unexpected"] = json!(true);
1276
- add(
1277
- format!("{op}/member/unexpected/added"),
1278
- "an unknown member is refused",
1279
- input(now, extra, records.clone()),
1280
- );
1281
- for key in records.as_object().unwrap().keys() {
1282
- let mut nulled = records.clone();
1283
- nulled[key] = Value::Null;
1284
- add(
1285
- format!("{op}/records/{key}/null"),
1286
- "a record key is present iff the record exists, never null",
1287
- input(now, operation.clone(), nulled),
1288
- );
1289
- }
1290
- let mut stray = records.clone();
1291
- stray["stray"] = json!({});
1292
- add(
1293
- format!("{op}/records/stray"),
1294
- "an unknown records member is refused",
1295
- input(now, operation.clone(), stray),
1296
- );
1297
- for field in ["contract", "now_ms", "operation", "records", "version"] {
1298
- let mut full = input(now, operation.clone(), records.clone());
1299
- full.as_object_mut().unwrap().remove(field);
1300
- add(
1301
- format!("{op}/envelope/{field}/removed"),
1302
- "the envelope has exactly five members",
1303
- full,
1304
- );
1305
- }
1306
- }
1307
-
1308
- // Identity and version.
1309
- for (name, contract, version) in [
1310
- ("wrong_contract", json!("feltdb.trigger.match"), json!(1)),
1311
- ("version_two", json!("feltdb.agent.transition"), json!(2)),
1312
- ("version_zero", json!("feltdb.agent.transition"), json!(0)),
1313
- (
1314
- "wrong_contract_and_version",
1315
- json!("feltdb.trigger.match"),
1316
- json!(9),
1317
- ),
1318
- ] {
1319
- let (now, operation, records) = base("checkpoint");
1320
- let mut full = input(now, operation, records);
1321
- full["contract"] = contract;
1322
- full["version"] = version;
1323
- add(
1324
- format!("identity/{name}"),
1325
- "identity and version are checked after shape",
1326
- full,
1327
- );
1328
- }
1329
- let (now, operation, records) = base("checkpoint");
1330
- let mut malformed_and_misaddressed =
1331
- input(now, with(operation, json!({"summary": null})), records);
1332
- malformed_and_misaddressed["contract"] = json!("other");
1333
- add(
1334
- "identity/malformed_beats_misaddressed".into(),
1335
- "shape errors win over identity errors on every implementation",
1336
- malformed_and_misaddressed,
1337
- );
1338
-
1339
- // create_task
1340
- let (now, op, _) = base("create_task");
1341
- let existing = json!({"task": task(|_| {})});
1342
- add(
1343
- "create_task/replay_is_a_noop".into(),
1344
- "the same creation repeated changes nothing",
1345
- input(now, op.clone(), existing.clone()),
1346
- );
1347
- add(
1348
- "create_task/other_objective_exists".into(),
1349
- "an existing task with another objective is not recreated",
1350
- input(
1351
- now,
1352
- with(op.clone(), json!({"objective": "Something else"})),
1353
- existing.clone(),
1354
- ),
1355
- );
1356
- add(
1357
- "create_task/other_actor_exists".into(),
1358
- "an existing task owned by another actor is not recreated",
1359
- input(
1360
- now,
1361
- with(op.clone(), json!({"actor": "agent-z"})),
1362
- existing.clone(),
1363
- ),
1364
- );
1365
- add(
1366
- "create_task/lease_cap_ok".into(),
1367
- "86,400,000 ms is the largest lease",
1368
- input(
1369
- now,
1370
- with(op.clone(), json!({"lease_ms": 86_400_000})),
1371
- json!({}),
1372
- ),
1373
- );
1374
- add(
1375
- "create_task/lease_over_cap".into(),
1376
- "one millisecond over the cap is invalid",
1377
- input(
1378
- now,
1379
- with(op.clone(), json!({"lease_ms": 86_400_001})),
1380
- json!({}),
1381
- ),
1382
- );
1383
- add(
1384
- "create_task/lease_zero".into(),
1385
- "a zero-length lease is invalid",
1386
- input(now, with(op.clone(), json!({"lease_ms": 0})), json!({})),
1387
- );
1388
- add(
1389
- "create_task/lease_overflow".into(),
1390
- "now + lease_ms must not overflow",
1391
- input(u64::MAX, op.clone(), json!({})),
1392
- );
1393
- add(
1394
- "create_task/now_max_is_fine_without_a_lease_sum".into(),
1395
- "now at u64::MAX alone is representable",
1396
- input(u64::MAX - 60000, op.clone(), json!({})),
1397
- );
1398
- let long = "x".repeat(1024);
1399
- add(
1400
- "create_task/objective_1024_bytes".into(),
1401
- "1,024 bytes is the limit",
1402
- input(now, with(op.clone(), json!({"objective": long})), json!({})),
1403
- );
1404
- add(
1405
- "create_task/objective_1025_bytes".into(),
1406
- "1,025 bytes is over",
1407
- input(
1408
- now,
1409
- with(op.clone(), json!({"objective": "x".repeat(1025)})),
1410
- json!({}),
1411
- ),
1412
- );
1413
- add(
1414
- "create_task/objective_multibyte_in_limit".into(),
1415
- "the limit is bytes: 341 three-byte characters is 1,023",
1416
- input(
1417
- now,
1418
- with(op.clone(), json!({"objective": "€".repeat(341)})),
1419
- json!({}),
1420
- ),
1421
- );
1422
- add(
1423
- "create_task/objective_multibyte_over".into(),
1424
- "342 three-byte characters is 1,026 bytes",
1425
- input(
1426
- now,
1427
- with(op.clone(), json!({"objective": "€".repeat(342)})),
1428
- json!({}),
1429
- ),
1430
- );
1431
- add(
1432
- "create_task/objective_empty".into(),
1433
- "empty text is invalid",
1434
- input(now, with(op.clone(), json!({"objective": ""})), json!({})),
1435
- );
1436
- for (name, id) in [
1437
- ("id_128", "a".repeat(128)),
1438
- ("id_129", "a".repeat(129)),
1439
- ("id_slash", "a/b".to_string()),
1440
- ("id_tilde", "a~b".to_string()),
1441
- ("id_space", "a b".to_string()),
1442
- ("id_empty", String::new()),
1443
- ("id_allowed_punctuation", "A_b.c:d@e-f".to_string()),
1444
- ("id_unicode", "tâche".to_string()),
1445
- ] {
1446
- add(
1447
- format!("create_task/{name}"),
1448
- "identifiers are 1..=128 bytes of [A-Za-z0-9_.:@-]",
1449
- input(now, with(op.clone(), json!({"task_id": id})), json!({})),
1450
- );
1451
- }
1452
-
1453
- // acquire
1454
- let (_, op, _) = base("acquire");
1455
- let expired = json!({"task": task(|_| {})});
1456
- add(
1457
- "acquire/at_exact_expiry".into(),
1458
- "a lease is expired when now >= expiresAt",
1459
- input(10000, op.clone(), expired.clone()),
1460
- );
1461
- add(
1462
- "acquire/one_ms_before_expiry".into(),
1463
- "one millisecond before expiry the lease is still held",
1464
- input(9999, op.clone(), expired.clone()),
1465
- );
1466
- add(
1467
- "acquire/same_actor_after_expiry".into(),
1468
- "the previous owner may re-acquire, with a new fence",
1469
- input(
1470
- 10000,
1471
- with(op.clone(), json!({"actor": "agent-a"})),
1472
- expired.clone(),
1473
- ),
1474
- );
1475
- add(
1476
- "acquire/reusing_the_lease_id".into(),
1477
- "a new lease needs a new id",
1478
- input(
1479
- 10000,
1480
- with(op.clone(), json!({"lease_id": "L2"})),
1481
- expired.clone(),
1482
- ),
1483
- );
1484
- add(
1485
- "acquire/terminal_completed".into(),
1486
- "a completed task is never acquired",
1487
- input(
1488
- 10000,
1489
- op.clone(),
1490
- json!({"task": terminal_task("completed")}),
1491
- ),
1492
- );
1493
- add(
1494
- "acquire/terminal_cancelled".into(),
1495
- "a cancelled task is never acquired",
1496
- input(
1497
- 10000,
1498
- op.clone(),
1499
- json!({"task": terminal_task("cancelled")}),
1500
- ),
1501
- );
1502
- add(
1503
- "acquire/missing_task".into(),
1504
- "no such task",
1505
- input(10000, op.clone(), json!({})),
1506
- );
1507
- add(
1508
- "acquire/fence_overflow".into(),
1509
- "the fence cannot wrap",
1510
- input(
1511
- 10000,
1512
- op.clone(),
1513
- json!({"task": task(|t| t["fence"] = json!(u64::MAX))}),
1514
- ),
1515
- );
1516
- add(
1517
- "acquire/blocked_task".into(),
1518
- "a blocked task can be acquired",
1519
- input(
1520
- 10000,
1521
- op.clone(),
1522
- json!({"task": task(|t| t["status"] = json!("blocked"))}),
1523
- ),
1524
- );
1525
-
1526
- // renew and the shared ownership order
1527
- let (_, op, _) = base("renew");
1528
- let owned = json!({"task": task(|_| {})});
1529
- add(
1530
- "renew/one_ms_before_expiry".into(),
1531
- "still held one millisecond before expiry",
1532
- input(9999, op.clone(), owned.clone()),
1533
- );
1534
- add(
1535
- "renew/at_exact_expiry".into(),
1536
- "expired at now == expiresAt",
1537
- input(10000, op.clone(), owned.clone()),
1538
- );
1539
- add(
1540
- "renew/stale_fence_lower".into(),
1541
- "an older fence is stale",
1542
- input(5000, with(op.clone(), json!({"fence": 1})), owned.clone()),
1543
- );
1544
- add(
1545
- "renew/stale_fence_higher".into(),
1546
- "a fence that is not the current one is stale in either direction",
1547
- input(5000, with(op.clone(), json!({"fence": 3})), owned.clone()),
1548
- );
1549
- add(
1550
- "renew/wrong_actor".into(),
1551
- "another actor does not own the task",
1552
- input(
1553
- 5000,
1554
- with(op.clone(), json!({"actor": "agent-z"})),
1555
- owned.clone(),
1556
- ),
1557
- );
1558
- add(
1559
- "renew/wrong_lease".into(),
1560
- "another lease id does not match",
1561
- input(
1562
- 5000,
1563
- with(op.clone(), json!({"lease_id": "L9"})),
1564
- owned.clone(),
1565
- ),
1566
- );
1567
- add(
1568
- "renew/order_fence_before_actor_and_lease".into(),
1569
- "stale fence, wrong actor and wrong lease report the fence",
1570
- input(
1571
- 5000,
1572
- with(
1573
- op.clone(),
1574
- json!({"fence": 1, "actor": "agent-z", "lease_id": "L9"}),
1575
- ),
1576
- owned.clone(),
1577
- ),
1578
- );
1579
- add(
1580
- "renew/order_actor_before_lease".into(),
1581
- "wrong actor and wrong lease report the actor",
1582
- input(
1583
- 5000,
1584
- with(op.clone(), json!({"actor": "agent-z", "lease_id": "L9"})),
1585
- owned.clone(),
1586
- ),
1587
- );
1588
- add(
1589
- "renew/order_lease_before_expiry".into(),
1590
- "wrong lease on an expired lease reports the mismatch",
1591
- input(
1592
- 10000,
1593
- with(op.clone(), json!({"lease_id": "L9"})),
1594
- owned.clone(),
1595
- ),
1596
- );
1597
- add(
1598
- "renew/terminal_before_fence".into(),
1599
- "a terminal task reports TERMINAL before anything else",
1600
- input(
1601
- 5000,
1602
- with(op.clone(), json!({"fence": 9, "actor": "agent-z"})),
1603
- json!({"task": terminal_task("completed")}),
1604
- ),
1605
- );
1606
- add(
1607
- "renew/missing_task".into(),
1608
- "no such task",
1609
- input(5000, op.clone(), json!({})),
1610
- );
1611
- add(
1612
- "renew/extends_from_now_not_from_expiry".into(),
1613
- "the new expiry is now + lease_ms",
1614
- input(
1615
- 9000,
1616
- with(op.clone(), json!({"lease_ms": 1})),
1617
- owned.clone(),
1618
- ),
1619
- );
1620
-
1621
- // checkpoint
1622
- let (_, op, _) = base("checkpoint");
1623
- let running = |evidence: Value| json!({"task": task(|_| {}), "evidence": evidence});
1624
- let status =
1625
- |from: &str| json!({"task": task(|t| t["status"] = json!(from)), "evidence": []});
1626
- add(
1627
- "checkpoint/same_status".into(),
1628
- "no status change is allowed while non-terminal",
1629
- input(5000, op.clone(), running(json!([]))),
1630
- );
1631
- for (from, to) in [
1632
- ("ready", "running"),
1633
- ("ready", "cancelled"),
1634
- ("ready", "completed"),
1635
- ("ready", "blocked"),
1636
- ("running", "ready"),
1637
- ("running", "blocked"),
1638
- ("running", "cancelled"),
1639
- ("blocked", "running"),
1640
- ("blocked", "cancelled"),
1641
- ("blocked", "completed"),
1642
- ("blocked", "ready"),
1643
- ] {
1644
- let evidence = if to == "completed" {
1645
- json!([rec("receipts", "r1")])
1646
- } else {
1647
- json!([])
1648
- };
1649
- let seen_list = if to == "completed" {
1650
- json!([seen("receipts", "r1", 1)])
1651
- } else {
1652
- json!([])
1653
- };
1654
- add(
1655
- format!("checkpoint/transition/{from}_to_{to}"),
1656
- "the lifecycle is an explicit table",
1657
- input(
1658
- 5000,
1659
- with(op.clone(), json!({"status": to, "evidence": evidence})),
1660
- json!({"task": task(|t| t["status"] = json!(from)), "evidence": seen_list}),
1661
- ),
1662
- );
1663
- }
1664
- for to in ["ready", "running", "blocked", "completed", "cancelled"] {
1665
- for from in ["completed", "cancelled"] {
1666
- add(
1667
- format!("checkpoint/terminal/{from}_to_{to}"),
1668
- "a terminal task takes no further checkpoints",
1669
- input(
1670
- 5000,
1671
- with(op.clone(), json!({"status": to})),
1672
- json!({"task": terminal_task(from), "evidence": []}),
1673
- ),
1674
- );
1675
- }
1676
- }
1677
- let _ = status;
1678
- add(
1679
- "checkpoint/version_conflict_lower".into(),
1680
- "expected_version must be the current checkpoint version",
1681
- input(
1682
- 5000,
1683
- with(op.clone(), json!({"expected_version": 2})),
1684
- running(json!([])),
1685
- ),
1686
- );
1687
- add(
1688
- "checkpoint/version_conflict_higher".into(),
1689
- "...in either direction",
1690
- input(
1691
- 5000,
1692
- with(op.clone(), json!({"expected_version": 4})),
1693
- running(json!([])),
1694
- ),
1695
- );
1696
- add(
1697
- "checkpoint/replay_of_applied_checkpoint".into(),
1698
- "a checkpoint replayed after it landed is a version conflict, not applied twice",
1699
- input(
1700
- 5000,
1701
- with(op.clone(), json!({"expected_version": 2})),
1702
- json!({"task": task(|_| {}), "evidence": []}),
1703
- ),
1704
- );
1705
- add(
1706
- "checkpoint/stale_fence".into(),
1707
- "an old owner cannot checkpoint",
1708
- input(
1709
- 5000,
1710
- with(op.clone(), json!({"fence": 1})),
1711
- running(json!([])),
1712
- ),
1713
- );
1714
- add(
1715
- "checkpoint/expired_lease".into(),
1716
- "an owner whose lease expired cannot checkpoint",
1717
- input(10000, op.clone(), running(json!([]))),
1718
- );
1719
- add(
1720
- "checkpoint/summary_1024".into(),
1721
- "summary limit in bytes",
1722
- input(
1723
- 5000,
1724
- with(op.clone(), json!({"summary": "s".repeat(1024)})),
1725
- running(json!([])),
1726
- ),
1727
- );
1728
- add(
1729
- "checkpoint/summary_1025".into(),
1730
- "over the limit",
1731
- input(
1732
- 5000,
1733
- with(op.clone(), json!({"summary": "s".repeat(1025)})),
1734
- running(json!([])),
1735
- ),
1736
- );
1737
- add(
1738
- "checkpoint/unknown_status".into(),
1739
- "status must be a lifecycle state",
1740
- input(
1741
- 5000,
1742
- with(op.clone(), json!({"status": "done"})),
1743
- running(json!([])),
1744
- ),
1745
- );
1746
- add(
1747
- "checkpoint/missing_task".into(),
1748
- "no such task",
1749
- input(5000, op.clone(), json!({"evidence": []})),
1750
- );
1751
- // evidence
1752
- let complete = with(op.clone(), json!({"status": "completed"}));
1753
- let cites = |refs: Value| with(complete.clone(), json!({"evidence": refs}));
1754
- add(
1755
- "checkpoint/complete_with_verified_record".into(),
1756
- "a completion cites a record FeltDB observed",
1757
- input(
1758
- 5000,
1759
- cites(json!([rec("receipts", "r1")])),
1760
- running(json!([seen("receipts", "r1", 4)])),
1761
- ),
1762
- );
1763
- add(
1764
- "checkpoint/complete_without_evidence".into(),
1765
- "INTENTIONAL BREAK: a completion with no evidence is refused",
1766
- input(5000, cites(json!([])), running(json!([]))),
1767
- );
1768
- add(
1769
- "checkpoint/complete_with_only_external_evidence".into(),
1770
- "an external URI is kept but is never verified, so it cannot back a completion",
1771
- input(5000, cites(json!([ext("git:abc123")])), running(json!([]))),
1772
- );
1773
- add(
1774
- "checkpoint/complete_with_unobserved_record".into(),
1775
- "a cited record FeltDB did not find cannot be cited",
1776
- input(
1777
- 5000,
1778
- cites(json!([rec("receipts", "r1")])),
1779
- running(json!([])),
1780
- ),
1781
- );
1782
- add(
1783
- "checkpoint/complete_citing_agent_record".into(),
1784
- "an agent's own record is not evidence for the agent",
1785
- input(
1786
- 5000,
1787
- cites(json!([rec("_feltdb.AgentCheckpoint", "t1~v3")])),
1788
- running(json!([seen("_feltdb.AgentCheckpoint", "t1~v3", 1)])),
1789
- ),
1790
- );
1791
- add(
1792
- "checkpoint/complete_with_unresolved_action".into(),
1793
- "an action that may or may not have run blocks completion",
1794
- input(
1795
- 5000,
1796
- cites(json!([rec("receipts", "r1")])),
1797
- json!({"task": with_unresolved(1), "evidence": [seen("receipts", "r1", 1)]}),
1798
- ),
1799
- );
1800
- add(
1801
- "checkpoint/complete_mixed_evidence".into(),
1802
- "one verified record among external citations is enough; the external stays unverified",
1803
- input(
1804
- 5000,
1805
- cites(json!([ext("git:abc123"), rec("receipts", "r1")])),
1806
- running(json!([seen("receipts", "r1", 2)])),
1807
- ),
1808
- );
1809
- add(
1810
- "checkpoint/evidence_duplicate".into(),
1811
- "the same citation twice is refused",
1812
- input(
1813
- 5000,
1814
- with(
1815
- op.clone(),
1816
- json!({"evidence": [rec("receipts", "r1"), rec("receipts", "r1")]}),
1817
- ),
1818
- running(json!([seen("receipts", "r1", 1)])),
1819
- ),
1820
- );
1821
- add(
1822
- "checkpoint/evidence_extra_observation".into(),
1823
- "an observation nobody cited is a malformed input",
1824
- input(
1825
- 5000,
1826
- op.clone(),
1827
- running(json!([seen("receipts", "r1", 1)])),
1828
- ),
1829
- );
1830
- add(
1831
- "checkpoint/evidence_duplicate_observation".into(),
1832
- "two observations of one record is a malformed input",
1833
- input(
1834
- 5000,
1835
- with(op.clone(), json!({"evidence": [rec("receipts", "r1")]})),
1836
- running(json!([
1837
- seen("receipts", "r1", 1),
1838
- seen("receipts", "r1", 2)
1839
- ])),
1840
- ),
1841
- );
1842
- add(
1843
- "checkpoint/evidence_16".into(),
1844
- "sixteen citations is the limit",
1845
- input(
1846
- 5000,
1847
- with(
1848
- op.clone(),
1849
- json!({"evidence": (0..16).map(|i| ext(&format!("u:{i}"))).collect::<Vec<_>>()}),
1850
- ),
1851
- running(json!([])),
1852
- ),
1853
- );
1854
- add(
1855
- "checkpoint/evidence_17".into(),
1856
- "seventeen is over",
1857
- input(
1858
- 5000,
1859
- with(
1860
- op.clone(),
1861
- json!({"evidence": (0..17).map(|i| ext(&format!("u:{i}"))).collect::<Vec<_>>()}),
1862
- ),
1863
- running(json!([])),
1864
- ),
1865
- );
1866
- add(
1867
- "checkpoint/evidence_record_with_uri".into(),
1868
- "a record citation carries no uri",
1869
- input(
1870
- 5000,
1871
- with(
1872
- op.clone(),
1873
- json!({"evidence": [{"kind": "record", "collection": "a", "id": "b", "uri": "x"}]}),
1874
- ),
1875
- running(json!([seen("a", "b", 1)])),
1876
- ),
1877
- );
1878
- add(
1879
- "checkpoint/evidence_external_with_id".into(),
1880
- "an external citation carries only a uri",
1881
- input(
1882
- 5000,
1883
- with(
1884
- op.clone(),
1885
- json!({"evidence": [{"kind": "external", "uri": "x", "id": "b"}]}),
1886
- ),
1887
- running(json!([])),
1888
- ),
1889
- );
1890
- add(
1891
- "checkpoint/evidence_verified_flag_is_not_an_input".into(),
1892
- "a caller cannot say a citation is verified",
1893
- input(
1894
- 5000,
1895
- with(
1896
- op.clone(),
1897
- json!({"evidence": [{"kind": "record", "collection": "a", "id": "b", "verified": true}]}),
1898
- ),
1899
- running(json!([seen("a", "b", 1)])),
1900
- ),
1901
- );
1902
- add(
1903
- "checkpoint/evidence_unknown_kind".into(),
1904
- "citation kinds are record and external",
1905
- input(
1906
- 5000,
1907
- with(
1908
- op.clone(),
1909
- json!({"evidence": [{"kind": "claim", "uri": "x"}]}),
1910
- ),
1911
- running(json!([])),
1912
- ),
1913
- );
1914
- add(
1915
- "checkpoint/evidence_guards_are_sorted".into(),
1916
- "guards are ordered by collection then id",
1917
- input(
1918
- 5000,
1919
- with(
1920
- op.clone(),
1921
- json!({"evidence": [rec("zeta", "a"), rec("alpha", "z"), rec("alpha", "b")]}),
1922
- ),
1923
- running(json!([
1924
- seen("zeta", "a", 1),
1925
- seen("alpha", "z", 2),
1926
- seen("alpha", "b", 3)
1927
- ])),
1928
- ),
1929
- );
1930
- add(
1931
- "checkpoint/cancel_with_unresolved_action".into(),
1932
- "cancelling is allowed with an unresolved action; the uncertainty stays recorded",
1933
- input(
1934
- 5000,
1935
- with(op.clone(), json!({"status": "cancelled"})),
1936
- json!({"task": with_unresolved(2), "evidence": []}),
1937
- ),
1938
- );
1939
-
1940
- // record_intent
1941
- let (_, op, _) = base("record_intent");
1942
- add(
1943
- "record_intent/recorded_again_while_intended".into(),
1944
- "an unresolved key is never a fresh start",
1945
- input(
1946
- 5000,
1947
- op.clone(),
1948
- json!({"task": with_unresolved(1), "intent": intent("intended")}),
1949
- ),
1950
- );
1951
- add(
1952
- "record_intent/recorded_again_while_reported".into(),
1953
- "a reported but unverified outcome is still uncertain",
1954
- input(
1955
- 5000,
1956
- op.clone(),
1957
- json!({"task": with_unresolved(1), "intent": intent("reported")}),
1958
- ),
1959
- );
1960
- add(
1961
- "record_intent/recorded_again_when_resolved".into(),
1962
- "a resolved key is not reused",
1963
- input(
1964
- 5000,
1965
- op.clone(),
1966
- json!({"task": task(|_| {}), "intent": intent("resolved")}),
1967
- ),
1968
- );
1969
- add(
1970
- "record_intent/task_blocked".into(),
1971
- "actions are started only while running",
1972
- input(
1973
- 5000,
1974
- op.clone(),
1975
- json!({"task": task(|t| t["status"] = json!("blocked"))}),
1976
- ),
1977
- );
1978
- add(
1979
- "record_intent/task_ready".into(),
1980
- "...not while ready",
1981
- input(
1982
- 5000,
1983
- op.clone(),
1984
- json!({"task": task(|t| t["status"] = json!("ready"))}),
1985
- ),
1986
- );
1987
- add(
1988
- "record_intent/stale_fence".into(),
1989
- "an old owner cannot begin an action",
1990
- input(
1991
- 5000,
1992
- with(op.clone(), json!({"fence": 1})),
1993
- json!({"task": task(|_| {})}),
1994
- ),
1995
- );
1996
- add(
1997
- "record_intent/expired_lease".into(),
1998
- "an expired owner cannot begin an action",
1999
- input(10000, op.clone(), json!({"task": task(|_| {})})),
2000
- );
2001
- add(
2002
- "record_intent/counter_overflow".into(),
2003
- "the unresolved count cannot wrap",
2004
- input(5000, op.clone(), json!({"task": with_unresolved(u64::MAX)})),
2005
- );
2006
- add(
2007
- "record_intent/missing_task".into(),
2008
- "no such task",
2009
- input(5000, op.clone(), json!({})),
2010
- );
2011
- add(
2012
- "record_intent/key_with_tilde".into(),
2013
- "~ separates composite ids, so it cannot appear in a key",
2014
- input(
2015
- 5000,
2016
- with(op.clone(), json!({"idempotency_key": "a~b"})),
2017
- json!({"task": task(|_| {})}),
2018
- ),
2019
- );
2020
-
2021
- // record_outcome
2022
- let (_, op, _) = base("record_outcome");
2023
- let waiting = |intent_state: &str, evidence: Value| json!({"task": with_unresolved(1), "intent": intent(intent_state), "evidence": evidence});
2024
- add(
2025
- "record_outcome/verified_resolves".into(),
2026
- "an outcome FeltDB saw evidence for resolves the action",
2027
- input(
2028
- 5000,
2029
- op.clone(),
2030
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2031
- ),
2032
- );
2033
- add(
2034
- "record_outcome/failed_with_evidence".into(),
2035
- "a failure with evidence resolves it too",
2036
- input(
2037
- 5000,
2038
- with(op.clone(), json!({"result": "failed"})),
2039
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2040
- ),
2041
- );
2042
- add(
2043
- "record_outcome/unverified_stays_uncertain".into(),
2044
- "an outcome with no evidence is only reported",
2045
- input(
2046
- 5000,
2047
- with(op.clone(), json!({"evidence": []})),
2048
- waiting("intended", json!([])),
2049
- ),
2050
- );
2051
- add(
2052
- "record_outcome/external_only_stays_uncertain".into(),
2053
- "external evidence does not verify an outcome",
2054
- input(
2055
- 5000,
2056
- with(
2057
- op.clone(),
2058
- json!({"evidence": [ext("https://shop/orders/9")]}),
2059
- ),
2060
- waiting("intended", json!([])),
2061
- ),
2062
- );
2063
- add(
2064
- "record_outcome/unobserved_record".into(),
2065
- "a record FeltDB did not find cannot verify an outcome",
2066
- input(5000, op.clone(), waiting("intended", json!([]))),
2067
- );
2068
- add(
2069
- "record_outcome/unknown_intent".into(),
2070
- "no intent was recorded",
2071
- input(
2072
- 5000,
2073
- op.clone(),
2074
- json!({"task": with_unresolved(1), "evidence": [seen("receipts", "r1", 1)]}),
2075
- ),
2076
- );
2077
- add(
2078
- "record_outcome/already_reported".into(),
2079
- "an outcome is recorded once; reconciliation settles the rest",
2080
- input(
2081
- 5000,
2082
- op.clone(),
2083
- waiting("reported", json!([seen("receipts", "r1", 1)])),
2084
- ),
2085
- );
2086
- add(
2087
- "record_outcome/already_resolved".into(),
2088
- "a resolved action is not changed",
2089
- input(
2090
- 5000,
2091
- op.clone(),
2092
- json!({"task": task(|_| {}), "intent": intent("resolved"), "evidence": [seen("receipts", "r1", 1)]}),
2093
- ),
2094
- );
2095
- add(
2096
- "record_outcome/counter_underflow".into(),
2097
- "an intended action with no counted intent is corrupt state",
2098
- input(
2099
- 5000,
2100
- op.clone(),
2101
- json!({"task": with_unresolved(0), "intent": intent("intended"), "evidence": [seen("receipts", "r1", 1)]}),
2102
- ),
2103
- );
2104
- add(
2105
- "record_outcome/bad_result".into(),
2106
- "a result is succeeded or failed",
2107
- input(
2108
- 5000,
2109
- with(op.clone(), json!({"result": "unknown"})),
2110
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2111
- ),
2112
- );
2113
- add(
2114
- "record_outcome/stale_fence".into(),
2115
- "an old owner cannot record an outcome",
2116
- input(
2117
- 5000,
2118
- with(op.clone(), json!({"fence": 1})),
2119
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2120
- ),
2121
- );
2122
-
2123
- // reconcile
2124
- let (_, op, _) = base("reconcile");
2125
- add(
2126
- "reconcile/executed_from_intended".into(),
2127
- "evidence that it ran settles it",
2128
- input(
2129
- 5000,
2130
- op.clone(),
2131
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2132
- ),
2133
- );
2134
- add(
2135
- "reconcile/not_executed_with_evidence".into(),
2136
- "evidence that it did not run settles it, and may permit a new attempt under a new key",
2137
- input(
2138
- 5000,
2139
- with(op.clone(), json!({"conclusion": "not_executed"})),
2140
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2141
- ),
2142
- );
2143
- add(
2144
- "reconcile/from_reported".into(),
2145
- "a reported outcome is settled by evidence, which accumulates",
2146
- input(
2147
- 5000,
2148
- op.clone(),
2149
- waiting("reported", json!([seen("receipts", "r1", 1)])),
2150
- ),
2151
- );
2152
- add(
2153
- "reconcile/not_executed_without_evidence".into(),
2154
- "the absence of an outcome record does not show it did not run",
2155
- input(
2156
- 5000,
2157
- with(
2158
- op.clone(),
2159
- json!({"conclusion": "not_executed", "evidence": []}),
2160
- ),
2161
- waiting("intended", json!([])),
2162
- ),
2163
- );
2164
- add(
2165
- "reconcile/executed_without_evidence".into(),
2166
- "...nor that it did",
2167
- input(
2168
- 5000,
2169
- with(op.clone(), json!({"evidence": []})),
2170
- waiting("intended", json!([])),
2171
- ),
2172
- );
2173
- add(
2174
- "reconcile/external_only".into(),
2175
- "external citations cannot settle an uncertain action",
2176
- input(
2177
- 5000,
2178
- with(
2179
- op.clone(),
2180
- json!({"evidence": [ext("https://shop/orders/9")]}),
2181
- ),
2182
- waiting("intended", json!([])),
2183
- ),
2184
- );
2185
- add(
2186
- "reconcile/already_resolved".into(),
2187
- "a resolved action is not reopened",
2188
- input(
2189
- 5000,
2190
- op.clone(),
2191
- json!({"task": task(|_| {}), "intent": intent("resolved"), "evidence": [seen("receipts", "r1", 1)]}),
2192
- ),
2193
- );
2194
- add(
2195
- "reconcile/unknown_intent".into(),
2196
- "no intent was recorded",
2197
- input(
2198
- 5000,
2199
- op.clone(),
2200
- json!({"task": with_unresolved(1), "evidence": [seen("receipts", "r1", 1)]}),
2201
- ),
2202
- );
2203
- add(
2204
- "reconcile/expired_lease".into(),
2205
- "only a current owner reconciles",
2206
- input(
2207
- 10000,
2208
- op.clone(),
2209
- waiting("intended", json!([seen("receipts", "r1", 1)])),
2210
- ),
2211
- );
2212
-
2213
- // create_handoff
2214
- let (_, op, _) = base("create_handoff");
2215
- add(
2216
- "create_handoff/open_to_anyone".into(),
2217
- "to is optional",
2218
- input(
2219
- 5000,
2220
- with(op.clone(), json!({"to": null})),
2221
- json!({"task": task(|_| {})}),
2222
- ),
2223
- );
2224
- add(
2225
- "create_handoff/already_exists".into(),
2226
- "a handoff id is used once",
2227
- input(
2228
- 5000,
2229
- op.clone(),
2230
- json!({"task": task(|_| {}), "handoff": handoff(|_| {})}),
2231
- ),
2232
- );
2233
- add(
2234
- "create_handoff/stale_fence".into(),
2235
- "only the current owner offers a handoff",
2236
- input(
2237
- 5000,
2238
- with(op.clone(), json!({"fence": 1})),
2239
- json!({"task": task(|_| {})}),
2240
- ),
2241
- );
2242
- add(
2243
- "create_handoff/expired_lease".into(),
2244
- "...holding an unexpired lease",
2245
- input(10000, op.clone(), json!({"task": task(|_| {})})),
2246
- );
2247
- add(
2248
- "create_handoff/terminal".into(),
2249
- "a terminal task is not handed off",
2250
- input(
2251
- 5000,
2252
- op.clone(),
2253
- json!({"task": terminal_task("completed")}),
2254
- ),
2255
- );
2256
-
2257
- // claim_handoff
2258
- let (_, op, _) = base("claim_handoff");
2259
- let open = json!({"task": task(|_| {}), "handoff": handoff(|_| {})});
2260
- add(
2261
- "claim_handoff/addressed_to_another".into(),
2262
- "a handoff addressed to B is not claimable by C",
2263
- input(
2264
- 5000,
2265
- with(op.clone(), json!({"claimant": "agent-c"})),
2266
- open.clone(),
2267
- ),
2268
- );
2269
- add(
2270
- "claim_handoff/open_to_anyone".into(),
2271
- "an unaddressed handoff is claimable by anyone",
2272
- input(
2273
- 5000,
2274
- with(op.clone(), json!({"claimant": "agent-c"})),
2275
- json!({"task": task(|_| {}), "handoff": handoff(|h| { h["to"] = Value::Null; h.as_object_mut().unwrap().remove("to"); })}),
2276
- ),
2277
- );
2278
- add(
2279
- "claim_handoff/stale_checkpoint_moved".into(),
2280
- "the task moved past the checkpoint the handoff names",
2281
- input(
2282
- 5000,
2283
- op.clone(),
2284
- json!({"task": task(|t| t["checkpoint_version"] = json!(4)), "handoff": handoff(|_| {})}),
2285
- ),
2286
- );
2287
- add(
2288
- "claim_handoff/stale_fence_moved".into(),
2289
- "the task was taken over since the handoff was offered",
2290
- input(
2291
- 5000,
2292
- op.clone(),
2293
- json!({"task": task(|t| t["fence"] = json!(3)), "handoff": handoff(|_| {})}),
2294
- ),
2295
- );
2296
- add(
2297
- "claim_handoff/stale_owner_moved".into(),
2298
- "the task's owner is no longer the offerer",
2299
- input(
2300
- 5000,
2301
- op.clone(),
2302
- json!({"task": task(|t| t["owner"] = json!("agent-z")), "handoff": handoff(|_| {})}),
2303
- ),
2304
- );
2305
- add(
2306
- "claim_handoff/claimed_and_live".into(),
2307
- "a live claim by another is respected",
2308
- input(
2309
- 5000,
2310
- op.clone(),
2311
- json!({"task": task(|t| { t["owner"] = json!("agent-c"); t["fence"] = json!(3); t["lease"] = json!({"expiresAt": 65000, "leaseId": "L4"}); }), "handoff": claimed_handoff("agent-c", 3)}),
2312
- ),
2313
- );
2314
- let held_by_b = json!({"task": task(|t| { t["owner"] = json!("agent-b"); t["fence"] = json!(3); t["lease"] = json!({"expiresAt": 65000, "leaseId": "L3"}); }), "handoff": claimed_handoff("agent-b", 3)});
2315
- add(
2316
- "claim_handoff/holder_retry_is_a_noop".into(),
2317
- "the holder retrying with the same lease id after an uncertain outcome changes nothing",
2318
- input(5000, op.clone(), held_by_b.clone()),
2319
- );
2320
- add(
2321
- "claim_handoff/holder_retry_other_lease".into(),
2322
- "...but not with another lease id",
2323
- input(
2324
- 5000,
2325
- with(op.clone(), json!({"lease_id": "L7"})),
2326
- held_by_b.clone(),
2327
- ),
2328
- );
2329
- add(
2330
- "claim_handoff/reclaim_after_expiry".into(),
2331
- "an abandoned claim can be taken after its lease expires",
2332
- input(
2333
- 65000,
2334
- with(op.clone(), json!({"claimant": "agent-c", "lease_id": "L8"})),
2335
- json!({"task": held_by_b["task"].clone(), "handoff": handoff_unaddressed_claimed("agent-b", 3)}),
2336
- ),
2337
- );
2338
- add(
2339
- "claim_handoff/reclaim_one_ms_early".into(),
2340
- "...not one millisecond before",
2341
- input(
2342
- 64999,
2343
- with(op.clone(), json!({"claimant": "agent-c", "lease_id": "L8"})),
2344
- json!({"task": held_by_b["task"].clone(), "handoff": handoff_unaddressed_claimed("agent-b", 3)}),
2345
- ),
2346
- );
2347
- add("claim_handoff/reclaim_after_owner_changed".into(), "an expired claim is recoverable only while the task is with the claimant that abandoned it", input(65000, with(op.clone(), json!({"claimant": "agent-c", "lease_id": "L8"})), json!({"task": task(|t| { t["owner"] = json!("agent-z"); t["fence"] = json!(4); t["lease"] = json!({"expiresAt": 1000, "leaseId": "L5"}); }), "handoff": handoff_unaddressed_claimed("agent-b", 3)})));
2348
- add(
2349
- "claim_handoff/accepted".into(),
2350
- "an accepted handoff is final",
2351
- input(
2352
- 5000,
2353
- op.clone(),
2354
- json!({"task": task(|_| {}), "handoff": handoff(|h| { h["status"] = json!("accepted"); h["claimant"] = json!("agent-b"); h["claimed_fence"] = json!(3); })}),
2355
- ),
2356
- );
2357
- add(
2358
- "claim_handoff/terminal_task".into(),
2359
- "a terminal task is not claimed",
2360
- input(
2361
- 5000,
2362
- op.clone(),
2363
- json!({"task": terminal_task("cancelled"), "handoff": handoff(|_| {})}),
2364
- ),
2365
- );
2366
- add(
2367
- "claim_handoff/reusing_the_lease_id".into(),
2368
- "the new lease needs a new id",
2369
- input(
2370
- 5000,
2371
- with(op.clone(), json!({"lease_id": "L2"})),
2372
- open.clone(),
2373
- ),
2374
- );
2375
- add(
2376
- "claim_handoff/missing_handoff".into(),
2377
- "no such handoff",
2378
- input(5000, op.clone(), json!({"task": task(|_| {})})),
2379
- );
2380
- add(
2381
- "claim_handoff/missing_task".into(),
2382
- "the handoff's task is gone",
2383
- input(5000, op.clone(), json!({"handoff": handoff(|_| {})})),
2384
- );
2385
- add(
2386
- "claim_handoff/task_mismatch".into(),
2387
- "the supplied task must be the handoff's task",
2388
- input(
2389
- 5000,
2390
- op.clone(),
2391
- json!({"task": task(|t| t["task_id"] = json!("t9")), "handoff": handoff(|_| {})}),
2392
- ),
2393
- );
2394
- add(
2395
- "claim_handoff/open_with_claimant".into(),
2396
- "an open handoff names no claimant: corrupt state",
2397
- input(
2398
- 5000,
2399
- op.clone(),
2400
- json!({"task": task(|_| {}), "handoff": handoff(|h| h["claimant"] = json!("agent-b"))}),
2401
- ),
2402
- );
2403
- add(
2404
- "claim_handoff/claimed_without_claimant".into(),
2405
- "a claimed handoff names its claimant: corrupt state",
2406
- input(
2407
- 5000,
2408
- op.clone(),
2409
- json!({"task": task(|_| {}), "handoff": handoff(|h| h["status"] = json!("claimed"))}),
2410
- ),
2411
- );
2412
-
2413
- // accept_handoff
2414
- let (_, op, accept_records) = base("accept_handoff");
2415
- let _ = accept_records;
2416
- let mine = json!({"task": task(|t| { t["fence"] = json!(3); t["owner"] = json!("agent-b"); t["lease"] = json!({"expiresAt": 65000, "leaseId": "L3"}); }), "handoff": claimed_handoff("agent-b", 3)});
2417
- add(
2418
- "accept_handoff/one_ms_before_expiry".into(),
2419
- "accepting in time",
2420
- input(64999, op.clone(), mine.clone()),
2421
- );
2422
- add(
2423
- "accept_handoff/at_exact_expiry".into(),
2424
- "the lease expired at now == expiresAt",
2425
- input(65000, op.clone(), mine.clone()),
2426
- );
2427
- add(
2428
- "accept_handoff/already_accepted_same_claimant".into(),
2429
- "accepting again changes nothing",
2430
- input(
2431
- 5000,
2432
- op.clone(),
2433
- json!({"task": mine["task"].clone(), "handoff": handoff(|h| { h["status"] = json!("accepted"); h["claimant"] = json!("agent-b"); h["claimed_fence"] = json!(3); })}),
2434
- ),
2435
- );
2436
- add(
2437
- "accept_handoff/already_accepted_by_another".into(),
2438
- "...but not by someone else",
2439
- input(
2440
- 5000,
2441
- with(op.clone(), json!({"claimant": "agent-c"})),
2442
- json!({"task": mine["task"].clone(), "handoff": handoff(|h| { h["status"] = json!("accepted"); h["claimant"] = json!("agent-b"); h["claimed_fence"] = json!(3); })}),
2443
- ),
2444
- );
2445
- add(
2446
- "accept_handoff/still_open".into(),
2447
- "an unclaimed handoff cannot be accepted",
2448
- input(
2449
- 5000,
2450
- op.clone(),
2451
- json!({"task": mine["task"].clone(), "handoff": handoff(|_| {})}),
2452
- ),
2453
- );
2454
- add(
2455
- "accept_handoff/not_the_claimant".into(),
2456
- "only the claimant accepts",
2457
- input(
2458
- 5000,
2459
- with(op.clone(), json!({"claimant": "agent-c"})),
2460
- mine.clone(),
2461
- ),
2462
- );
2463
- add(
2464
- "accept_handoff/stale_fence".into(),
2465
- "the claimant's fence must be current",
2466
- input(5000, with(op.clone(), json!({"fence": 2})), mine.clone()),
2467
- );
2468
- add(
2469
- "accept_handoff/claim_superseded".into(),
2470
- "the task was taken over after the claim: the claim is stale",
2471
- input(
2472
- 5000,
2473
- op.clone(),
2474
- json!({"task": task(|t| { t["fence"] = json!(4); t["owner"] = json!("agent-b"); t["lease"] = json!({"expiresAt": 65000, "leaseId": "L3"}); }), "handoff": claimed_handoff("agent-b", 3)}),
2475
- ),
2476
- );
2477
- add(
2478
- "accept_handoff/wrong_lease".into(),
2479
- "the lease id must be the task's",
2480
- input(
2481
- 5000,
2482
- with(op.clone(), json!({"lease_id": "L9"})),
2483
- mine.clone(),
2484
- ),
2485
- );
2486
- add(
2487
- "accept_handoff/missing_handoff".into(),
2488
- "no such handoff",
2489
- input(5000, op.clone(), json!({"task": mine["task"].clone()})),
2490
- );
2491
-
2492
- // Raw encodings.
2493
- let valid = serde_json::to_string(&input(
2494
- 5000,
2495
- base_op("renew"),
2496
- json!({"task": task(|_| {})}),
2497
- ))
2498
- .unwrap();
2499
- let mut raw_cases: Vec<(&str, &str, String)> = vec![
2500
- ("raw/empty", "no input at all", String::new()),
2501
- ("raw/not_json", "not JSON", "this is not json".into()),
2502
- ("raw/null", "the JSON null", "null".into()),
2503
- ("raw/array", "the JSON array", "[]".into()),
2504
- (
2505
- "raw/empty_object",
2506
- "an empty object has no envelope members",
2507
- "{}".into(),
2508
- ),
2509
- (
2510
- "raw/truncated",
2511
- "a truncated document",
2512
- valid[..valid.len() / 2].to_string(),
2513
- ),
2514
- (
2515
- "raw/trailing_garbage",
2516
- "content after the document",
2517
- format!("{valid} x"),
2518
- ),
2519
- (
2520
- "raw/two_documents",
2521
- "two documents",
2522
- format!("{valid}{valid}"),
2523
- ),
2524
- (
2525
- "raw/pretty_printed_valid",
2526
- "whitespace is not semantics",
2527
- serde_json::to_string_pretty(&serde_json::from_str::<Value>(&valid).unwrap())
2528
- .unwrap(),
2529
- ),
2530
- (
2531
- "raw/leading_bom",
2532
- "a BOM is not JSON",
2533
- format!("\u{feff}{valid}"),
2534
- ),
2535
- (
2536
- "raw/duplicate_envelope_member",
2537
- "a repeated member is refused",
2538
- valid.replacen("\"now_ms\":5000", "\"now_ms\":5000,\"now_ms\":5000", 1),
2539
- ),
2540
- (
2541
- "raw/duplicate_operation_member",
2542
- "...inside the operation",
2543
- valid.replacen("\"op\":\"renew\"", "\"op\":\"renew\",\"op\":\"renew\"", 1),
2544
- ),
2545
- (
2546
- "raw/duplicate_records_member",
2547
- "...inside records",
2548
- valid.replacen("\"records\":{", "\"records\":{\"task\":null,", 1),
2549
- ),
2550
- (
2551
- "raw/duplicate_lease_member",
2552
- "...inside a stored lease",
2553
- valid.replacen(
2554
- "\"expiresAt\":10000",
2555
- "\"expiresAt\":10000,\"expiresAt\":1",
2556
- 1,
2557
- ),
2558
- ),
2559
- (
2560
- "raw/float_for_integer",
2561
- "1.0 is not an integer",
2562
- valid.replacen("\"now_ms\":5000", "\"now_ms\":5000.0", 1),
2563
- ),
2564
- (
2565
- "raw/exponent_for_integer",
2566
- "5e3 is not an integer",
2567
- valid.replacen("\"now_ms\":5000", "\"now_ms\":5e3", 1),
2568
- ),
2569
- (
2570
- "raw/negative_zero",
2571
- "-0 is not a non-negative integer",
2572
- valid.replacen("\"now_ms\":5000", "\"now_ms\":-0", 1),
2573
- ),
2574
- (
2575
- "raw/negative",
2576
- "negative integers are refused",
2577
- valid.replacen("\"now_ms\":5000", "\"now_ms\":-5", 1),
2578
- ),
2579
- (
2580
- "raw/leading_zero",
2581
- "leading zeros are not JSON",
2582
- valid.replacen("\"now_ms\":5000", "\"now_ms\":05000", 1),
2583
- ),
2584
- (
2585
- "raw/u64_max",
2586
- "u64::MAX is representable",
2587
- valid.replacen("\"now_ms\":5000", "\"now_ms\":18446744073709551615", 1),
2588
- ),
2589
- (
2590
- "raw/u64_max_plus_one",
2591
- "one more is not",
2592
- valid.replacen("\"now_ms\":5000", "\"now_ms\":18446744073709551616", 1),
2593
- ),
2594
- (
2595
- "raw/escaped_identifier_ok",
2596
- "escapes decode before validation",
2597
- valid.replacen("\"agent-a\"", "\"agent\\u002da\"", 1),
2598
- ),
2599
- (
2600
- "raw/escaped_identifier_invalid_char",
2601
- "...so an escaped space is still a space",
2602
- valid.replacen("\"agent-a\"", "\"agent\\u0020a\"", 1),
2603
- ),
2604
- (
2605
- "raw/lone_high_surrogate",
2606
- "a lone surrogate is not text",
2607
- valid.replacen("\"agent-a\"", "\"\\ud800\"", 1),
2608
- ),
2609
- (
2610
- "raw/lone_low_surrogate",
2611
- "...nor a lone trailing one",
2612
- valid.replacen("\"agent-a\"", "\"\\udc00\"", 1),
2613
- ),
2614
- (
2615
- "raw/raw_control_character",
2616
- "an unescaped control character in a string",
2617
- valid.replacen("\"agent-a\"", "\"agent\u{1}a\"", 1),
2618
- ),
2619
- (
2620
- "raw/depth_bomb",
2621
- "excessive nesting is an input error, not a crash",
2622
- format!("{}{}", "[".repeat(300), "]".repeat(300)),
2623
- ),
2624
- (
2625
- "raw/summary_with_escapes_and_astral",
2626
- "text with escapes and an astral character is counted in UTF-8 bytes",
2627
- String::new(),
2628
- ),
2629
- ];
2630
- let checkpoint = serde_json::to_string(&{
2631
- let (now, operation, records) = base("checkpoint");
2632
- input(
2633
- now,
2634
- with(
2635
- operation,
2636
- json!({"summary": "line\nbreak \"quoted\" \\ tab\t \u{1} 😀 é"}),
2637
- ),
2638
- records,
2639
- )
2640
- })
2641
- .unwrap();
2642
- raw_cases.last_mut().unwrap().2 = checkpoint;
2643
- raw_cases.push(("raw/output_escapes_control_and_unicode_in_text", "the writer escapes exactly as a canonical encoder does", serde_json::to_string(&{
2644
- let (now, operation, records) = base("create_task");
2645
- input(now, with(operation, json!({"objective": "q\"uote \\ slash \u{7f} del \u{8}\u{c}\n\r\t \u{1f} €😀"})), records)
2646
- }).unwrap()));
2647
- raw_cases.push(("raw/permuted_member_order", "member order is not semantics", {
2648
- let (now, operation, records) = base("renew");
2649
- format!(
2650
- "{{\"version\":1,\"records\":{},\"operation\":{},\"now_ms\":{now},\"contract\":\"feltdb.agent.transition\"}}",
2651
- serde_json::to_string(&records).unwrap(),
2652
- serde_json::to_string(&operation).unwrap()
2653
- )
2654
- }));
2655
- for (name, why, text) in raw_cases {
2656
- out.push(json!({"name": name, "why": why, "raw_input": text}));
2657
- }
2658
- out
2659
- }
2660
-
2661
- fn handoff_unaddressed_claimed(claimant: &str, fence: u64) -> Value {
2662
- handoff(|h| {
2663
- h.as_object_mut().unwrap().remove("to");
2664
- h["status"] = json!("claimed");
2665
- h["claimant"] = json!(claimant);
2666
- h["claimed_fence"] = json!(fence);
2667
- })
2668
- }
2669
-
2670
- // -- sequences -----------------------------------------------------------
2671
-
2672
- struct Step(&'static str, u64, Value, Vec<(&'static str, &'static str)>);
2673
-
2674
- fn step(label: &'static str, now: u64, operation: Value) -> Step {
2675
- Step(label, now, operation, vec![])
2676
- }
2677
-
2678
- fn seeded(mut s: Step, seeds: &[(&'static str, &'static str)]) -> Step {
2679
- s.3 = seeds.to_vec();
2680
- s
2681
- }
2682
-
2683
- fn sequence(name: &str, why: &str, steps: Vec<Step>) -> Value {
2684
- let steps: Vec<Value> = steps
2685
- .into_iter()
2686
- .map(|Step(label, now, operation, seeds)| {
2687
- json!({
2688
- "label": label, "now_ms": now, "operation": operation,
2689
- "seed": seeds.iter().map(|(c, i)| json!({"collection": c, "id": i})).collect::<Vec<_>>(),
2690
- })
2691
- })
2692
- .collect();
2693
- json!({"name": name, "why": why, "steps": steps})
2694
- }
2695
-
2696
- fn create(actor: &str, lease: &str) -> Value {
2697
- json!({"op": "create_task", "task_id": "t1", "actor": actor, "objective": "Ship it", "lease_id": lease, "lease_ms": 1000})
2698
- }
2699
- fn ck(
2700
- actor: &str,
2701
- fence: u64,
2702
- lease: &str,
2703
- version: u64,
2704
- status: Option<&str>,
2705
- evidence: Value,
2706
- ) -> Value {
2707
- let mut op = json!({"op": "checkpoint", "task_id": "t1", "actor": actor, "fence": fence, "lease_id": lease, "expected_version": version, "summary": "progress", "evidence": evidence});
2708
- if let Some(s) = status {
2709
- op["status"] = json!(s);
2710
- }
2711
- op
2712
- }
2713
- fn intent_op(actor: &str, fence: u64, lease: &str, key: &str) -> Value {
2714
- json!({"op": "record_intent", "task_id": "t1", "actor": actor, "fence": fence, "lease_id": lease, "idempotency_key": key, "action": "charge-card"})
2715
- }
2716
- fn outcome_op(actor: &str, fence: u64, lease: &str, key: &str, evidence: Value) -> Value {
2717
- json!({"op": "record_outcome", "task_id": "t1", "actor": actor, "fence": fence, "lease_id": lease, "idempotency_key": key, "result": "succeeded", "evidence": evidence})
2718
- }
2719
- fn reconcile_op(
2720
- actor: &str,
2721
- fence: u64,
2722
- lease: &str,
2723
- key: &str,
2724
- conclusion: &str,
2725
- evidence: Value,
2726
- ) -> Value {
2727
- json!({"op": "reconcile", "task_id": "t1", "actor": actor, "fence": fence, "lease_id": lease, "idempotency_key": key, "conclusion": conclusion, "evidence": evidence})
2728
- }
2729
- fn acquire_op(actor: &str, lease: &str) -> Value {
2730
- json!({"op": "acquire", "task_id": "t1", "actor": actor, "lease_id": lease, "lease_ms": 1000})
2731
- }
2732
- fn renew_op(actor: &str, fence: u64, lease: &str) -> Value {
2733
- json!({"op": "renew", "task_id": "t1", "actor": actor, "fence": fence, "lease_id": lease, "lease_ms": 1000})
2734
- }
2735
- fn offer(actor: &str, fence: u64, lease: &str, to: Option<&str>) -> Value {
2736
- let mut op = json!({"op": "create_handoff", "task_id": "t1", "handoff_id": "h1", "actor": actor, "fence": fence, "lease_id": lease});
2737
- if let Some(to) = to {
2738
- op["to"] = json!(to);
2739
- }
2740
- op
2741
- }
2742
- fn claim(who: &str, lease: &str) -> Value {
2743
- json!({"op": "claim_handoff", "handoff_id": "h1", "claimant": who, "lease_id": lease, "lease_ms": 1000})
2744
- }
2745
- fn accept(who: &str, fence: u64, lease: &str) -> Value {
2746
- json!({"op": "accept_handoff", "handoff_id": "h1", "claimant": who, "fence": fence, "lease_id": lease})
2747
- }
2748
-
2749
- pub fn sequences() -> Vec<Value> {
2750
- let receipt = || json!([rec("receipts", "r1")]);
2751
- vec![
2752
- sequence("lifecycle_happy_path", "create, start, act, verify the outcome, complete with evidence, and nothing more", vec![
2753
- step("create", 0, create("agent-a", "L1")),
2754
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2755
- step("intend", 200, intent_op("agent-a", 1, "L1", "charge-1")),
2756
- seeded(step("outcome_verified", 300, outcome_op("agent-a", 1, "L1", "charge-1", receipt())), &[("receipts", "r1")]),
2757
- step("complete", 400, ck("agent-a", 1, "L1", 2, Some("completed"), receipt())),
2758
- step("terminal_checkpoint_refused", 500, ck("agent-a", 1, "L1", 3, None, json!([]))),
2759
- step("terminal_renew_refused", 500, renew_op("agent-a", 1, "L1")),
2760
- step("terminal_acquire_refused", 5000, acquire_op("agent-b", "L2")),
2761
- ]),
2762
- sequence("completion_requires_verified_evidence", "a completion is a claim until FeltDB has seen a record that backs it", vec![
2763
- step("create", 0, create("agent-a", "L1")),
2764
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2765
- step("no_evidence", 200, ck("agent-a", 1, "L1", 2, Some("completed"), json!([]))),
2766
- step("external_only", 210, ck("agent-a", 1, "L1", 2, Some("completed"), json!([ext("git:abc123")]))),
2767
- step("record_not_there_yet", 220, ck("agent-a", 1, "L1", 2, Some("completed"), receipt())),
2768
- step("own_checkpoint_as_evidence", 230, ck("agent-a", 1, "L1", 2, Some("completed"), json!([rec("_feltdb.AgentCheckpoint", "t1~v2")]))),
2769
- seeded(step("verified_record", 240, ck("agent-a", 1, "L1", 2, Some("completed"), receipt())), &[("receipts", "r1")]),
2770
- ]),
2771
- sequence("evidence_is_pinned_to_the_observed_version", "a citation records the version FeltDB saw; a later change to the record is a later version", vec![
2772
- step("create", 0, create("agent-a", "L1")),
2773
- seeded(step("cite_version_1", 100, ck("agent-a", 1, "L1", 1, Some("running"), receipt())), &[("receipts", "r1")]),
2774
- seeded(step("cite_version_2_after_change", 200, ck("agent-a", 1, "L1", 2, None, receipt())), &[("receipts", "r1")]),
2775
- ]),
2776
- sequence("crash_after_side_effect_before_outcome", "the action ran, the process died before recording it, and the successor must not assume either way", vec![
2777
- step("create", 0, create("agent-a", "L1")),
2778
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2779
- step("intend", 200, intent_op("agent-a", 1, "L1", "charge-1")),
2780
- step("owner_is_lost", 1500, ck("agent-a", 1, "L1", 2, None, json!([]))),
2781
- step("successor_acquires", 1600, acquire_op("agent-b", "L2")),
2782
- step("replaying_the_action_is_refused", 1700, intent_op("agent-b", 2, "L2", "charge-1")),
2783
- seeded(step("completing_is_refused", 1710, ck("agent-b", 2, "L2", 2, Some("completed"), json!([rec("audit", "x")]))), &[("audit", "x")]),
2784
- step("reconcile_needs_evidence", 1720, reconcile_op("agent-b", 2, "L2", "charge-1", "executed", json!([]))),
2785
- seeded(step("reconcile_with_the_receipt", 1730, reconcile_op("agent-b", 2, "L2", "charge-1", "executed", receipt())), &[("receipts", "r1")]),
2786
- step("the_key_is_spent", 1740, intent_op("agent-b", 2, "L2", "charge-1")),
2787
- step("complete", 1750, ck("agent-b", 2, "L2", 2, Some("completed"), receipt())),
2788
- ]),
2789
- sequence("confirmed_non_execution_permits_a_new_attempt", "only evidence of non-execution releases the uncertainty, and the retry is a new action under a new key", vec![
2790
- step("create", 0, create("agent-a", "L1")),
2791
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2792
- step("intend", 200, intent_op("agent-a", 1, "L1", "charge-1")),
2793
- step("absence_is_not_evidence", 300, reconcile_op("agent-a", 1, "L1", "charge-1", "not_executed", json!([]))),
2794
- seeded(step("gateway_has_no_charge", 310, reconcile_op("agent-a", 1, "L1", "charge-1", "not_executed", json!([rec("gateway_audit", "a1")]))), &[("gateway_audit", "a1")]),
2795
- step("same_key_is_not_reused", 320, intent_op("agent-a", 1, "L1", "charge-1")),
2796
- step("new_attempt", 330, intent_op("agent-a", 1, "L1", "charge-2")),
2797
- seeded(step("new_attempt_verified", 340, outcome_op("agent-a", 1, "L1", "charge-2", receipt())), &[("receipts", "r1")]),
2798
- step("complete", 350, ck("agent-a", 1, "L1", 2, Some("completed"), receipt())),
2799
- ]),
2800
- sequence("an_unverified_outcome_stays_uncertain", "an outcome somebody reported is not an outcome FeltDB observed", vec![
2801
- step("create", 0, create("agent-a", "L1")),
2802
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2803
- step("intend", 200, intent_op("agent-a", 1, "L1", "charge-1")),
2804
- step("reported_without_evidence", 300, outcome_op("agent-a", 1, "L1", "charge-1", json!([]))),
2805
- step("reported_outcome_cannot_be_recorded_twice", 310, outcome_op("agent-a", 1, "L1", "charge-1", json!([]))),
2806
- seeded(step("completion_refused", 320, ck("agent-a", 1, "L1", 2, Some("completed"), json!([rec("audit", "x")]))), &[("audit", "x")]),
2807
- seeded(step("reconcile_with_evidence", 330, reconcile_op("agent-a", 1, "L1", "charge-1", "executed", receipt())), &[("receipts", "r1")]),
2808
- step("complete", 340, ck("agent-a", 1, "L1", 2, Some("completed"), receipt())),
2809
- ]),
2810
- sequence("a_stale_owner_cannot_act_after_takeover", "once another owner holds the task, the old owner's fence is dead for every operation", vec![
2811
- step("create", 0, create("agent-a", "L1")),
2812
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2813
- step("acquire_too_early", 999, acquire_op("agent-b", "L2")),
2814
- step("acquire_at_expiry", 1100, acquire_op("agent-b", "L2")),
2815
- step("old_owner_checkpoint", 1200, ck("agent-a", 1, "L1", 2, None, json!([]))),
2816
- step("old_owner_renew", 1200, renew_op("agent-a", 1, "L1")),
2817
- step("old_owner_intent", 1200, intent_op("agent-a", 1, "L1", "k")),
2818
- step("old_owner_handoff", 1200, offer("agent-a", 1, "L1", None)),
2819
- step("old_owner_with_new_fence", 1200, ck("agent-a", 2, "L2", 2, None, json!([]))),
2820
- step("new_owner_checkpoint", 1300, ck("agent-b", 2, "L2", 2, None, json!([]))),
2821
- ]),
2822
- sequence("lease_boundaries", "a lease is live strictly before expiresAt and expired at it, for renewal and for work", vec![
2823
- step("create", 0, create("agent-a", "L1")),
2824
- step("renew_one_ms_before_expiry", 999, renew_op("agent-a", 1, "L1")),
2825
- step("work_one_ms_before_new_expiry", 1998, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2826
- step("work_at_exact_expiry", 1999, ck("agent-a", 1, "L1", 2, None, json!([]))),
2827
- step("renew_at_exact_expiry", 1999, renew_op("agent-a", 1, "L1")),
2828
- step("acquire_one_ms_early", 1998, acquire_op("agent-b", "L2")),
2829
- step("acquire_at_expiry", 1999, acquire_op("agent-b", "L2")),
2830
- ]),
2831
- sequence("handoff_claim_and_accept", "offer, a wrong claimant is refused, the right one claims, the offerer is fenced out, and acceptance is final", vec![
2832
- step("create", 0, create("agent-a", "L1")),
2833
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2834
- step("offer", 200, offer("agent-a", 1, "L1", Some("agent-b"))),
2835
- step("offer_again", 210, offer("agent-a", 1, "L1", Some("agent-b"))),
2836
- step("wrong_claimant", 300, claim("agent-c", "L9")),
2837
- step("claim", 400, claim("agent-b", "L2")),
2838
- step("claim_again_by_another", 410, claim("agent-c", "L3")),
2839
- step("offerer_is_fenced_out", 420, ck("agent-a", 1, "L1", 2, None, json!([]))),
2840
- step("accept", 500, accept("agent-b", 2, "L2")),
2841
- step("accept_again", 510, accept("agent-b", 2, "L2")),
2842
- step("claim_after_accept", 520, claim("agent-b", "L5")),
2843
- ]),
2844
- sequence("handoff_abandoned_claim_is_recoverable_once", "an abandoned claim can be taken after expiry, and the first claimant is then fenced out", vec![
2845
- step("create", 0, create("agent-a", "L1")),
2846
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2847
- step("offer_to_anyone", 200, offer("agent-a", 1, "L1", None)),
2848
- step("b_claims", 300, claim("agent-b", "L2")),
2849
- step("c_cannot_claim_while_live", 400, claim("agent-c", "L3")),
2850
- step("c_claims_after_expiry", 1300, claim("agent-c", "L3")),
2851
- step("b_cannot_accept", 1400, accept("agent-b", 2, "L2")),
2852
- step("c_accepts", 1500, accept("agent-c", 3, "L3")),
2853
- ]),
2854
- sequence("handoff_goes_stale_when_the_task_moves_on", "a handoff names a checkpoint; progress past it makes the offer stale", vec![
2855
- step("create", 0, create("agent-a", "L1")),
2856
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2857
- step("offer", 200, offer("agent-a", 1, "L1", None)),
2858
- step("owner_moves_on", 300, ck("agent-a", 1, "L1", 2, None, json!([]))),
2859
- step("claim_is_stale", 400, claim("agent-b", "L2")),
2860
- ]),
2861
- sequence("repeated_operations", "which repeats are no-ops, which are refused, and which would be a different action", vec![
2862
- step("create", 0, create("agent-a", "L1")),
2863
- step("create_again_is_a_noop", 10, create("agent-a", "L1")),
2864
- step("create_with_another_objective", 20, json!({"op": "create_task", "task_id": "t1", "actor": "agent-a", "objective": "Other", "lease_id": "L1", "lease_ms": 1000})),
2865
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2866
- step("the_same_checkpoint_again_conflicts", 110, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2867
- step("intend", 200, intent_op("agent-a", 1, "L1", "k")),
2868
- step("intend_again_is_uncertain", 210, intent_op("agent-a", 1, "L1", "k")),
2869
- step("offer", 300, offer("agent-a", 1, "L1", Some("agent-b"))),
2870
- step("claim", 400, claim("agent-b", "L2")),
2871
- step("claim_retry_by_holder_is_a_noop", 410, claim("agent-b", "L2")),
2872
- step("accept", 500, accept("agent-b", 2, "L2")),
2873
- step("accept_retry_is_a_noop", 510, accept("agent-b", 2, "L2")),
2874
- ]),
2875
- sequence("cancel_keeps_uncertainty_visible", "cancelling is allowed with an unresolved action and the count stays on the terminal task", vec![
2876
- step("create", 0, create("agent-a", "L1")),
2877
- step("start", 100, ck("agent-a", 1, "L1", 1, Some("running"), json!([]))),
2878
- step("intend", 200, intent_op("agent-a", 1, "L1", "k")),
2879
- step("cancel", 300, ck("agent-a", 1, "L1", 2, Some("cancelled"), json!([]))),
2880
- step("no_late_reconcile_in_v1", 310, reconcile_op("agent-a", 1, "L1", "k", "executed", json!([]))),
2881
- ]),
2882
- ]
2883
- }
2884
- }