@feltdb/core 0.13.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 (300) hide show
  1. package/dist/agent-runtime.d.ts +64 -2
  2. package/dist/agent-runtime.d.ts.map +1 -1
  3. package/dist/agent-runtime.js +121 -11
  4. package/dist/agent-state.d.ts +68 -0
  5. package/dist/agent-state.d.ts.map +1 -1
  6. package/dist/agent-state.js +210 -12
  7. package/dist/agent.d.ts +10 -1
  8. package/dist/agent.d.ts.map +1 -1
  9. package/dist/agent.js +10 -1
  10. package/dist/build-identity.json +12 -4
  11. package/dist/cli/commands.js +214 -10
  12. package/dist/cli/publish-engine.js +116 -16
  13. package/dist/collection.d.ts +3 -1
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +33 -8
  16. package/dist/create/build-identity.json +12 -4
  17. package/dist/create/cli.js +1 -1
  18. package/dist/create/create.js +4 -6
  19. package/dist/create/managed-account.js +93 -35
  20. package/dist/create/package-versions.js +1 -1
  21. package/dist/create/server-source/Cargo.lock +8 -511
  22. package/dist/create/server-source/Cargo.toml +2 -13
  23. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  24. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  25. package/dist/create/server-source/crates/feltdb-server/README.md +6 -1
  26. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +9 -1
  27. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +85 -25
  28. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +4 -0
  29. package/dist/create/server-source/crates/feltdb-server/src/console_accounts.rs +1009 -0
  30. package/dist/create/server-source/crates/feltdb-server/src/control_authorization.rs +441 -0
  31. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +68 -6
  32. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +355 -23
  33. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +3 -0
  34. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1134 -96
  35. package/dist/create/server-source/crates/feltdb-server/src/redaction.rs +208 -0
  36. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +198 -19
  37. package/dist/create/server-source/crates/feltdb-server/tests/transaction_precondition_contract.rs +403 -0
  38. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  39. package/dist/db.d.ts +75 -10
  40. package/dist/db.d.ts.map +1 -1
  41. package/dist/db.js +168 -29
  42. package/dist/deployment.d.ts +19 -0
  43. package/dist/deployment.d.ts.map +1 -1
  44. package/dist/deployment.js +38 -2
  45. package/dist/embedded-query.d.ts.map +1 -1
  46. package/dist/embedded-query.js +5 -4
  47. package/dist/embedded-transaction.d.ts +8 -4
  48. package/dist/embedded-transaction.d.ts.map +1 -1
  49. package/dist/embedded-transaction.js +55 -19
  50. package/dist/error-codes.d.ts +36 -2
  51. package/dist/error-codes.d.ts.map +1 -1
  52. package/dist/error-codes.js +50 -3
  53. package/dist/feltdb.d.ts +7 -0
  54. package/dist/feltdb.d.ts.map +1 -1
  55. package/dist/file-db.d.ts +27 -0
  56. package/dist/file-db.d.ts.map +1 -1
  57. package/dist/file-db.js +145 -7
  58. package/dist/http-client.d.ts +11 -3
  59. package/dist/http-client.d.ts.map +1 -1
  60. package/dist/http-client.js +27 -14
  61. package/dist/http-db.d.ts +43 -1
  62. package/dist/http-db.d.ts.map +1 -1
  63. package/dist/http-db.js +287 -59
  64. package/dist/index.js +1 -1
  65. package/dist/local-state-lock.d.ts +34 -0
  66. package/dist/local-state-lock.d.ts.map +1 -0
  67. package/dist/local-state-lock.js +225 -0
  68. package/dist/managed-recovery.d.ts +6 -0
  69. package/dist/managed-recovery.d.ts.map +1 -1
  70. package/dist/managed-recovery.js +9 -0
  71. package/dist/reactive-graph.d.ts +0 -3
  72. package/dist/reactive-graph.d.ts.map +1 -1
  73. package/dist/reactive-graph.js +11 -1
  74. package/dist/runtime-registry.d.ts +7 -2
  75. package/dist/runtime-registry.d.ts.map +1 -1
  76. package/dist/runtime-registry.js +2 -2
  77. package/dist/studio/components/index.js +1 -1
  78. package/dist/studio/{components-BSGmHfif.js → components-CJmzntBi.js} +2 -1
  79. package/dist/studio/index.js +1 -1
  80. package/dist/studio-app/assets/{feltdb_wasm-3hDngl1h.js → feltdb_wasm-DtybUY0q.js} +1 -1
  81. package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
  82. package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
  83. package/dist/studio-app/index.html +1 -1
  84. package/dist/transaction.d.ts +27 -11
  85. package/dist/transaction.d.ts.map +1 -1
  86. package/dist/transaction.js +20 -14
  87. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  88. package/dist/wasm/package.json +1 -1
  89. package/dist/workload.d.ts +86 -6
  90. package/dist/workload.d.ts.map +1 -1
  91. package/dist/workload.js +51 -3
  92. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  93. package/dist/workspace/workspace-connection.js +14 -3
  94. package/package.json +1 -1
  95. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  96. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  97. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  98. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  99. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  100. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  101. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  102. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  103. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  104. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  105. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  106. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  107. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  108. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  109. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  110. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  111. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  112. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  113. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  114. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  115. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  116. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  117. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  118. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  119. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  120. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  121. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  122. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  123. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  124. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  125. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  126. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  127. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  128. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  129. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  130. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  131. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  132. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  133. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  134. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  135. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  136. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  137. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  138. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  139. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  140. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  141. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  142. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  143. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  144. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  145. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  146. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  147. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  148. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  149. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  150. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  151. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  152. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  153. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  154. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  155. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  156. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  157. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  158. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  159. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  160. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  161. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  162. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  163. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7274
  164. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  165. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  166. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  167. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  168. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  169. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  170. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  171. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  172. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  173. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  174. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  175. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  176. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  177. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  178. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  179. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  180. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  181. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  182. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  183. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  184. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  185. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  186. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  187. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  188. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  189. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  190. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  191. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  192. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  193. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  194. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  195. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  196. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  197. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  198. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  199. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  200. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  201. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  202. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  203. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  204. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  205. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  206. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  207. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  208. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  209. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  210. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  211. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  212. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  213. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -5894
  214. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  215. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  216. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  217. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  218. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  219. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  220. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  221. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  222. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  223. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  224. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  225. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  226. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  227. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  228. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  229. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  230. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  231. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  232. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1133
  233. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  234. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  235. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  236. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  237. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -2886
  238. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  239. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  240. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  241. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  242. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  243. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  244. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  245. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  246. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  247. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  248. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  249. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  250. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  251. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  252. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  253. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  254. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  255. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  256. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  257. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  258. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  259. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  260. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  261. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  262. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  263. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  264. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  265. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  266. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  267. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  268. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  269. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  270. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  271. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  272. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  273. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  274. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  275. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  276. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  277. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  278. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  279. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  280. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  281. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  282. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  283. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  284. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  285. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  286. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  287. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  288. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  289. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  290. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  291. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  292. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  293. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  294. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  295. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  296. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  297. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  298. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  299. package/dist/studio-app/assets/feltdb_wasm_bg-CFQT4gCy.wasm +0 -0
  300. package/dist/studio-app/assets/index-Dql1hS5m.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
- }