@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.
- package/dist/agent-runtime.d.ts +64 -2
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +121 -11
- package/dist/agent-state.d.ts +68 -0
- package/dist/agent-state.d.ts.map +1 -1
- package/dist/agent-state.js +210 -12
- package/dist/agent.d.ts +10 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -1
- package/dist/build-identity.json +12 -4
- package/dist/cli/commands.js +214 -10
- package/dist/cli/publish-engine.js +116 -16
- package/dist/collection.d.ts +3 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +33 -8
- package/dist/create/build-identity.json +12 -4
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +4 -6
- package/dist/create/managed-account.js +93 -35
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +8 -511
- package/dist/create/server-source/Cargo.toml +2 -13
- package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
- package/dist/create/server-source/crates/feltdb-server/README.md +6 -1
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +9 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +85 -25
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +4 -0
- package/dist/create/server-source/crates/feltdb-server/src/console_accounts.rs +1009 -0
- package/dist/create/server-source/crates/feltdb-server/src/control_authorization.rs +441 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +68 -6
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +355 -23
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +3 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1134 -96
- package/dist/create/server-source/crates/feltdb-server/src/redaction.rs +208 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +198 -19
- package/dist/create/server-source/crates/feltdb-server/tests/transaction_precondition_contract.rs +403 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
- package/dist/db.d.ts +75 -10
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +168 -29
- package/dist/deployment.d.ts +19 -0
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +38 -2
- package/dist/embedded-query.d.ts.map +1 -1
- package/dist/embedded-query.js +5 -4
- package/dist/embedded-transaction.d.ts +8 -4
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +55 -19
- package/dist/error-codes.d.ts +36 -2
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +50 -3
- package/dist/feltdb.d.ts +7 -0
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +27 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +145 -7
- package/dist/http-client.d.ts +11 -3
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +27 -14
- package/dist/http-db.d.ts +43 -1
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +287 -59
- package/dist/index.js +1 -1
- package/dist/local-state-lock.d.ts +34 -0
- package/dist/local-state-lock.d.ts.map +1 -0
- package/dist/local-state-lock.js +225 -0
- package/dist/managed-recovery.d.ts +6 -0
- package/dist/managed-recovery.d.ts.map +1 -1
- package/dist/managed-recovery.js +9 -0
- package/dist/reactive-graph.d.ts +0 -3
- package/dist/reactive-graph.d.ts.map +1 -1
- package/dist/reactive-graph.js +11 -1
- package/dist/runtime-registry.d.ts +7 -2
- package/dist/runtime-registry.d.ts.map +1 -1
- package/dist/runtime-registry.js +2 -2
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-BSGmHfif.js → components-CJmzntBi.js} +2 -1
- package/dist/studio/index.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-3hDngl1h.js → feltdb_wasm-DtybUY0q.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
- package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +27 -11
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +20 -14
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/package.json +1 -1
- package/dist/workload.d.ts +86 -6
- package/dist/workload.d.ts.map +1 -1
- package/dist/workload.js +51 -3
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +14 -3
- package/package.json +1 -1
- package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
- package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
- package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
- package/dist/create/server-source/crates/feltdb/README.md +0 -75
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
- package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
- package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
- package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
- package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
- package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
- package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
- package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
- package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
- package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
- package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7274
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
- package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
- package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
- package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
- package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
- package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
- package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
- package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
- package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -5894
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
- package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
- package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
- package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
- package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1133
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
- package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -2886
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
- package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
- package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
- package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
- package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
- package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
- package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
- package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
- package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
- package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
- package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
- package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
- package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
- package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
- package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
- package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
- package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
- package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
- package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
- package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
- package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
- package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
- package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
- package/dist/studio-app/assets/feltdb_wasm_bg-CFQT4gCy.wasm +0 -0
- package/dist/studio-app/assets/index-Dql1hS5m.js +0 -30
package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs
DELETED
|
@@ -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
|
-
}
|