test(session-types): the CONTRACT-01 fixtures, cross-checks and their Rust tests

fixtures/ is the shared contract corpus: accepted payloads with their canonical JSON and
digest, refused payloads with the rule each breaks, raw byte sequences that must not even
parse, recipes for the payloads too large to store, the U64 and double boundaries, rational
arithmetic vectors, the identity matrix, descriptor cross-checks, operation keys and bodies,
trace comparison variants, the schema set and its digest, seed vectors and one FLYSESS1
envelope. packages/session-types loads the same files.

Tests: 45 across nine files. Round trips preserve every field and reproduce the recorded
canonical bytes; duplicate keys, invalid UTF-8, NaN, trailing data, oversize typed values and
oversize envelopes fail; U64 and double boundaries reject on the right side; the four
identities never accept one another spellings; the accumulator produces 16, 17, 17 and a zero
remainder; the trace comparator ignores wall time, request ids, callIds and delivery ids and
notices every behaviour change; the contract digest survives reformatting and moves when a
field, bound, enum member or type changes.

Also: a crate README, and the update_fixtures example that regenerates the derived files.
This commit is contained in:
acamilo 2026-09-22 11:41:04 +00:00
parent d2b916602e
commit 1652afad11
26 changed files with 13501 additions and 0 deletions

View file

@ -0,0 +1,103 @@
# fly-session-types
The executable schemas of the session framework: domain scalars, closed enums, method
payloads, canonical JSON, canonical digests and the step trace format.
This crate is CONTRACT-01 of
[`docs/design/session-framework/implementation.md`](../../../../docs/design/session-framework/implementation.md).
It holds no transport, no worker, no coordinator and no store; it never opens a socket or a
file other than its own fixtures. The bus owns the wire
([`flybus`](../flybus)), and this crate owns what the messages mean.
## Layout
| Module | Contents |
| --- | --- |
| `scalar` | `Scope`, `RationalNs`, `SchemaRef`, `TypedValue`, the `DomainType` trait, and `BusCallId` / `DomainRequestId` / `ArtifactIdentity` / `OwnerToken` |
| `canonical` | RFC 8785 canonical JSON, SHA-256 digests, `OperationKey`, canonical bodies, the 64-KiB envelope check |
| `rpc` | `SessionRpcRequest`, `SessionRpcSuccess`, `SessionRpcFailure`, `ErrorCode`, `MutationCertainty` |
| `workers` | The closed enums and every Agent/Environment/Worker method payload of workers-v1 |
| `media` | `ViewDescriptor`, `ViewRef`, `AudioDescriptor`, `AudioRef` and the `State.*` payloads |
| `publishing` | `SessionDescriptor` and `CommittedSnapshot` |
| `trace` | `TraceBehaviour`, `TraceOperational`, `TransitionTrace` and the behaviour comparator |
| `schema` | The canonical schema set and `contract_digest()` |
| `seed` | `seed-derivation-v1` |
| `checkpoint` | The `FLYSESS1` envelope layout |
| `fixtures` | Loading `fixtures/`, shared with `packages/session-types` |
`Id`, `U64` and `Digest` are the bus encodings: `scalar` calls into `flybus::wire` instead of
restating them, and `tests/encodings.rs` pins that the two agree for every edge case.
## Reading and validating
Every type implements `DomainType`:
```rust
use fly_session_types::scalar::{DomainType, Scope};
let scope = Scope::from_json(&value)?; // reads, refusing unknown fields, then validates
scope.validate()?; // the cross-field rules, re-runnable
let json = scope.to_json(); // the canonical shape
```
Rules that need another value in hand are separate, because a payload cannot check them alone:
```rust
control.validate_against(&port.controls)?; // complete batch, descriptor order, ranges
input.validate_against(&descriptor.views)?; // max(0, boundary - observationDelaySteps)
result.validate_against(&descriptor, &previous)?; // exactly one stepDuration of world time
snapshot.validate_against(&session_descriptor)?; // revision, agent set, assigned ports
telemetry.validate_against_roles(&profile_roles)?; // rates in profile-defined order
```
## Digests
- `contract_digest()` is the SHA-256 of the canonical schema set (`schema::schema_set()`),
which is a declaration: type names, JSON field names, kinds, bounds and closed enums.
Reformatting this crate cannot change it; changing a field or a bound does.
- `canonical::body_digest(method, scope, params)` is the comparison ipc-v1 section 5 uses to
tell a safe replay from a `CONFLICT`. It refuses a body that carries a bus identity.
- `OperationKey` is `(sessionId, epoch, step, method, workerId)`, and deliberately not the
request id: a changed id for an existing key is the conflict to detect.
## Fixtures
`fixtures/` is loaded by these tests and by `packages/session-types`, so a case is written
once and holds both languages to it.
| File | Contents |
| --- | --- |
| `valid.json` | Payloads every implementation accepts, with their canonical JSON and digest |
| `invalid.json` | Payloads every implementation refuses, each with the rule it breaks |
| `raw.json` | Byte sequences refused before validation: duplicate keys, invalid UTF-8, `NaN`, trailing data |
| `generated.json` | Recipes for payloads too large to store: the 32-KiB and 64-KiB boundaries, 512-code-point messages |
| `boundaries.json` | The `U64` decimal-string and double boundaries |
| `rational.json` | Checked rational arithmetic and the 16, 17, 17 tick accumulator |
| `identities.json` | Which of the four identity types accepts which spelling |
| `descriptor-checks.json` | Rules that need a descriptor: batches, delays, byte shapes, descriptor agreement |
| `operations.json` | Operation keys, canonical bodies and the pairs that are or are not the same operation |
| `traces.json` | A baseline transition and the variants that must or must not compare equal |
| `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest |
| `seed-vectors.json` | `seed-derivation-v1` test vectors |
| `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses |
The derived files (`schema-set.json`, `contract-digest.json`, the `canonical`/`digest` fields
of `valid.json`, the digests in `operations.json`, `seed-vectors.json` and
`checkpoint-envelope.json`) come from
`cargo run -p fly-session-types --example update_fixtures`;
`tests/schema_set.rs` fails if the checked-in files are stale.
## Tests
```sh
cargo test -p fly-session-types
cargo clippy -p fly-session-types --all-targets
```
## Bounds this crate chose
Every bound in the schema set names its source. Six are marked `crate` because no document
states them: `maxAudioStreams` (8), `maxCapabilities` (32), `maxSupportedMajors` (8),
`maxSupportedStimuli` (64), `maxAssets` (64) and `maxSnapshotEvents` (64). They exist so an
unbounded array cannot fill an envelope, and they are in the digest, so widening one is a
contract change rather than a quiet edit.

View file

@ -0,0 +1,290 @@
//! Regenerates the derived fixture files.
//!
//! `cargo run -p fly-session-types --example update_fixtures`. `tests/fixtures_current.rs`
//! fails if the checked-in files differ from what this writes, so the digests in the
//! fixtures can never drift from the code that produced them.
use std::collections::BTreeMap;
use fly_session_types::scalar::{DomainType, Scope};
use fly_session_types::{canonical, checkpoint, fixtures, schema, seed};
use serde_json::{Map, Value, json};
fn main() {
let dir = fixtures::dir();
for (name, contents) in derived() {
let path = dir.join(&name);
std::fs::write(&path, contents).expect("write fixture");
println!("wrote {}", path.display());
}
}
/// Every derived fixture, as `(file name, exact bytes)`.
pub fn derived() -> Vec<(String, String)> {
vec![
("schema-set.json".to_owned(), schema_set()),
("contract-digest.json".to_owned(), contract_digest()),
("valid.json".to_owned(), valid()),
("operations.json".to_owned(), operations()),
("seed-vectors.json".to_owned(), seed_vectors()),
("checkpoint-envelope.json".to_owned(), checkpoint_envelope()),
]
}
fn write(value: &Value) -> String {
let mut text = serde_json::to_string_pretty(value).expect("serializable");
text.push('\n');
text
}
fn schema_set() -> String {
// The rendered set is itself canonical JSON, so the file the TypeScript package hashes is
// byte for byte what the digest was taken over.
let mut text = schema::schema_set_json().expect("canonicalizable");
text.push('\n');
text
}
fn contract_digest() -> String {
let set = schema::schema_set_json().expect("canonicalizable");
write(&json!({
"description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.",
"contractDigest": schema::contract_digest(),
"schemaSetVersion": schema::SCHEMA_SET_VERSION,
"schemaSetBytes": set.len(),
"types": schema::SCHEMAS.len(),
"enums": schema::ENUMS.len(),
"limits": schema::LIMITS.len(),
}))
}
fn valid() -> String {
let mut file = fixtures::load("valid.json").expect("valid.json");
let cases = file
.get_mut("cases")
.and_then(Value::as_array_mut)
.expect("cases");
for case in cases.iter_mut() {
let value = case.get("value").expect("value").clone();
let canonical = canonical::canonicalize(&value).expect("canonicalizable");
let digest = canonical::sha256_hex(canonical.as_bytes());
let map = case.as_object_mut().expect("case object");
map.insert("canonical".to_owned(), Value::String(canonical));
map.insert("digest".to_owned(), Value::String(digest));
}
write(&file)
}
fn operations() -> String {
let mut file = fixtures::load("operations.json").expect("operations.json");
let scope_of = |case: &Value| -> Option<Scope> {
match case.get("scope") {
Some(Value::Null) | None => None,
Some(v) => Some(Scope::from_json(v).expect("scope")),
}
};
for key in file
.get_mut("keys")
.and_then(Value::as_array_mut)
.expect("keys")
{
let scope = scope_of(key).expect("an operation key has a scope");
let method = key.get("method").and_then(Value::as_str).expect("method");
let worker = key.get("workerId").and_then(Value::as_str).expect("workerId");
let digest = canonical::OperationKey::new(scope, method, worker)
.expect("valid key")
.digest()
.expect("digest");
key.as_object_mut()
.expect("object")
.insert("digest".to_owned(), Value::String(digest));
}
for body in file
.get_mut("bodies")
.and_then(Value::as_array_mut)
.expect("bodies")
{
let scope = scope_of(body);
let method = body.get("method").and_then(Value::as_str).expect("method");
let params = body.get("params").expect("params").clone();
let digest =
canonical::body_digest(method, scope.as_ref(), &params).expect("canonical body");
body.as_object_mut()
.expect("object")
.insert("digest".to_owned(), Value::String(digest));
}
write(&file)
}
fn seed_vectors() -> String {
let master_seeds: [u64; 5] = [0, 1, 42, 9_223_372_036_854_775_808, u64::MAX];
let agents = ["fly-a", "fly-b", "fly-c", "fly-d"];
let mut vectors = Vec::new();
for master in master_seeds {
for agent in agents {
let material = seed::material(master, agent).expect("material");
vectors.push(json!({
"masterSeed": master.to_string(),
"agentId": agent,
"material": String::from_utf8(material).expect("utf-8"),
"materialDigest": seed::material_digest(master, agent).expect("digest"),
"seed": seed::agent_seed(master, agent).expect("seed"),
}));
}
}
let composition: Vec<Value> = seed::composition_seeds(
42,
&agents.iter().map(|a| (*a).to_owned()).collect::<Vec<_>>(),
)
.expect("composition")
.into_iter()
.map(Value::from)
.collect();
write(&json!({
"description": "seed-derivation-v1 test vectors. Both languages must reproduce every seed.",
"algorithm": seed::ALGORITHM,
"prefix": seed::PREFIX,
"materialTemplate": "<prefix>\\n<masterSeed>\\n<agentId>\\n",
"rule": "SHA-256 of the material, read as eight big-endian u32 lanes; the first nonzero lane is the seed as a two's-complement i32.",
"vectors": vectors,
"composition": {
"masterSeed": "42",
"agentIds": agents,
"seeds": composition,
"reason": "independent per-agent seeds from one recorded master seed and stable agent ids",
},
"invalid": [
{"masterSeed": "0", "agentId": "Fly-A", "reason": "an agent id is an Id: lowercase"},
{"masterSeed": "0", "agentId": "", "reason": "an agent id is 1..=64 characters"},
{"masterSeed": "0", "agentIds": ["fly-a", "fly-a"],
"reason": "a composition with a repeated agent id is refused rather than silently sharing a seed"},
],
}))
}
fn checkpoint_envelope() -> String {
let scope = Scope::new("demo", "epoch-1", 42).expect("scope");
let manifest = json!({
"envelopeVersion": checkpoint::VERSION,
"checkpointId": "ckpt-1",
"sourceScope": scope.to_json(),
"episodeId": "episode-1",
"worldTime": {"numerator": "700000000", "denominator": "1"},
"schedulerId": "lockstep-v1",
"compositionDigest": canonical::sha256_hex(b"composition"),
"portMap": [{"portId": "port-1", "agentId": "fly-a"}],
"compatibility": {
"backendDigest": canonical::sha256_hex(b"backend"),
"contentDigest": canonical::sha256_hex(b"content"),
"patchDigest": canonical::sha256_hex(b"patch"),
"controllerDigest": canonical::sha256_hex(b"controller"),
"parserDigest": canonical::sha256_hex(b"parser"),
"stateFormatId": "flysess-1",
},
"agents": [{
"agentId": "fly-a",
"profileDigest": canonical::sha256_hex(b"profile"),
"datasetDigest": canonical::sha256_hex(b"fafb-v783"),
"modelVersion": "lif-1ms-f64-v2",
"plasticityVersion": "fly-kc-mbon-rstdp-v2",
"seed": seed::agent_seed(42, "fly-a").expect("seed"),
"brainTicks": "2534",
"remainder": {"numerator": "1000000", "denominator": "3"},
"payload": "agent-fly-a",
}],
"coordinator": {
"taskLedger": "task-ledger",
"priorInspection": "prior-inspection",
"executorState": [{"agentId": "fly-a", "payload": "executor-fly-a"}],
"admissionState": null,
"eventWatermarks": {"lastEventId": "evt-1", "lastOrdinal": "7"},
},
"helperState": [],
"payloads": payload_table(),
});
let bytes = checkpoint::encode(&manifest, &payloads()).expect("encode");
let envelope = checkpoint::decode(&bytes).expect("decode");
let entries: Vec<Value> = envelope
.layout
.entries
.iter()
.map(|entry| {
json!({
"name": entry.name,
"offset": entry.offset.to_string(),
"byteLength": entry.byte_length.to_string(),
"digest": checkpoint::hex(&entry.digest),
})
})
.collect();
let first_payload = envelope.layout.entries[0].offset;
write(&json!({
"description": "One FLYSESS1 envelope, its layout and the corruptions a reader must refuse.",
"magic": "FLYSESS1",
"footerMagic": "FLYSESSF",
"version": checkpoint::VERSION,
"manifest": manifest,
"payloads": payloads()
.iter()
.map(|(name, bytes)| json!({"name": name, "base64": fixtures::encode_base64(bytes)}))
.collect::<Vec<_>>(),
"envelope": {
"base64": fixtures::encode_base64(&bytes),
"byteLength": bytes.len(),
"layout": {
"headerBytes": checkpoint::HEADER_BYTES,
"manifestOffset": envelope.layout.manifest_offset.to_string(),
"manifestBytes": envelope.layout.manifest_bytes,
"tableOffset": envelope.layout.table_offset.to_string(),
"tableEntryBytes": checkpoint::TABLE_ENTRY_BYTES,
"entries": entries,
"footerOffset": envelope.layout.footer_offset.to_string(),
"footerBytes": checkpoint::FOOTER_BYTES,
"totalBytes": envelope.layout.total_bytes.to_string(),
},
},
"corruption": [
{"name": "a flipped magic byte", "offset": 0, "reason": "wrong magic"},
{"name": "an unsupported version", "offset": 8, "reason": "unsupported version"},
{"name": "a flipped manifest byte", "offset": checkpoint::HEADER_BYTES,
"reason": "the footer digest covers the manifest"},
{"name": "a flipped payload byte", "offset": first_payload,
"reason": "every payload carries its own digest"},
{"name": "a flipped footer digest byte", "offset": bytes.len() - 40,
"reason": "the footer digest must match the contents"},
{"name": "a flipped footer magic byte", "offset": bytes.len() - 8,
"reason": "a truncated file cannot look complete"},
],
}))
}
fn payloads() -> Vec<(String, Vec<u8>)> {
vec![
("agent-fly-a".to_owned(), b"agent state bytes".to_vec()),
("executor-fly-a".to_owned(), b"executor state".to_vec()),
("task-ledger".to_owned(), b"{\"rank\":10}".to_vec()),
("prior-inspection".to_owned(), b"{\"map\":40}".to_vec()),
("world".to_owned(), vec![0u8; 64]),
]
}
fn payload_table() -> Value {
let mut out = Vec::new();
for (name, bytes) in payloads() {
let mut entry = Map::new();
entry.insert("name".to_owned(), Value::String(name));
entry.insert(
"byteLength".to_owned(),
Value::String(bytes.len().to_string()),
);
entry.insert(
"digest".to_owned(),
Value::String(canonical::sha256_hex(&bytes)),
);
out.push(Value::Object(entry));
}
// A BTreeMap would sort the payload names; the table order is the write order, which is
// what the envelope records.
let _: BTreeMap<(), ()> = BTreeMap::new();
Value::Array(out)
}

View file

@ -0,0 +1,153 @@
{
"description": "The U64 decimal-string and double boundaries, shared by both languages.",
"u64": [
{
"text": "0",
"accept": true,
"reason": "zero is \"0\""
},
{
"text": "1",
"accept": true,
"reason": ""
},
{
"text": "18446744073709551615",
"accept": true,
"reason": "the U64 maximum"
},
{
"text": "18446744073709551616",
"accept": false,
"reason": "one past the maximum"
},
{
"text": "184467440737095516150",
"accept": false,
"reason": "far past the maximum"
},
{
"text": "00",
"accept": false,
"reason": "no leading zeros"
},
{
"text": "01",
"accept": false,
"reason": "no leading zeros"
},
{
"text": "",
"accept": false,
"reason": "empty"
},
{
"text": "-1",
"accept": false,
"reason": "unsigned"
},
{
"text": "+1",
"accept": false,
"reason": "no sign"
},
{
"text": "1.0",
"accept": false,
"reason": "integers only"
},
{
"text": "1e3",
"accept": false,
"reason": "decimal digits only"
},
{
"text": " 1",
"accept": false,
"reason": "no whitespace"
},
{
"text": "1 ",
"accept": false,
"reason": "no whitespace"
},
{
"text": "0x10",
"accept": false,
"reason": "decimal only"
},
{
"text": "9007199254740993",
"accept": true,
"reason": "a U64 string keeps precision a double would lose"
}
],
"doubles": [
{
"value": 0.0,
"canonical": "0",
"accept": true,
"reason": ""
},
{
"value": -0.0,
"canonical": "0",
"accept": true,
"reason": "JSON.stringify prints negative zero as 0"
},
{
"value": 1.0,
"canonical": "1",
"accept": true,
"reason": "an integral double prints without a fraction"
},
{
"value": 0.1,
"canonical": "0.1",
"accept": true,
"reason": ""
},
{
"value": 1e+21,
"canonical": "1e+21",
"accept": true,
"reason": "ECMAScript switches to exponent notation at 1e21"
},
{
"value": 1e-07,
"canonical": "1e-7",
"accept": true,
"reason": ""
},
{
"value": 5e-324,
"canonical": "5e-324",
"accept": true,
"reason": "the smallest subnormal double"
},
{
"value": 1.7976931348623157e+308,
"canonical": "1.7976931348623157e+308",
"accept": true,
"reason": "the largest finite double"
},
{
"value": 9007199254740991,
"canonical": "9007199254740991",
"accept": true,
"reason": "the largest exactly representable integer"
},
{
"value": 9007199254740993,
"canonical": null,
"accept": false,
"reason": "past the exact integer range, canonical JSON refuses it"
},
{
"value": 0.30000000000000004,
"canonical": "0.30000000000000004",
"accept": true,
"reason": "shortest round-tripping form, not a rounded one"
}
]
}

View file

@ -0,0 +1,195 @@
{
"description": "One FLYSESS1 envelope, its layout and the corruptions a reader must refuse.",
"magic": "FLYSESS1",
"footerMagic": "FLYSESSF",
"version": 1,
"manifest": {
"envelopeVersion": 1,
"checkpointId": "ckpt-1",
"sourceScope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "42"
},
"episodeId": "episode-1",
"worldTime": {
"numerator": "700000000",
"denominator": "1"
},
"schedulerId": "lockstep-v1",
"compositionDigest": "730d725c8a59d3a7303def2bed041a577edb4255aabd4889ce12918311d952f0",
"portMap": [
{
"portId": "port-1",
"agentId": "fly-a"
}
],
"compatibility": {
"backendDigest": "10e08a419e850eba1ebba18fdd28eb7ec1b7e8baa9bcc3b973e2b8891ec726be",
"contentDigest": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
"patchDigest": "a4895eb44afc336fecbba6e520cd67e178dace0276655d102fceffa8e5f70570",
"controllerDigest": "c1472135b14c77c8bef98e73f70208325fa0dcf1e6bd668ae9b31a9cea295fe7",
"parserDigest": "b17d45121150928f2146af49e195eff1eef5d67325be273a733fb74acadaa342",
"stateFormatId": "flysess-1"
},
"agents": [
{
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"datasetDigest": "6c0af1f0784ef63a393ee77d614e8246c625051360f3f1a48838374c5d355b52",
"modelVersion": "lif-1ms-f64-v2",
"plasticityVersion": "fly-kc-mbon-rstdp-v2",
"seed": -184946063,
"brainTicks": "2534",
"remainder": {
"numerator": "1000000",
"denominator": "3"
},
"payload": "agent-fly-a"
}
],
"coordinator": {
"taskLedger": "task-ledger",
"priorInspection": "prior-inspection",
"executorState": [
{
"agentId": "fly-a",
"payload": "executor-fly-a"
}
],
"admissionState": null,
"eventWatermarks": {
"lastEventId": "evt-1",
"lastOrdinal": "7"
}
},
"helperState": [],
"payloads": [
{
"name": "agent-fly-a",
"byteLength": "17",
"digest": "1321dffb0cdc6f9092cbf7fa2a5fc68bbed12c993d5ad398264012810ce9bf93"
},
{
"name": "executor-fly-a",
"byteLength": "14",
"digest": "3aee60df7e29efeba7f5f99fc5867647b36aebff1d5d3c838dbff323122e6462"
},
{
"name": "task-ledger",
"byteLength": "11",
"digest": "40b00ed2bbba901d68205ff71b04a44b9ee53c51cb3109aa2ceaa44f1c45727e"
},
{
"name": "prior-inspection",
"byteLength": "10",
"digest": "2c13b7b4d9a9916801ab9191c314f31b045e9b9c5b669a6c0474f0217ef75bf5"
},
{
"name": "world",
"byteLength": "64",
"digest": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
}
]
},
"payloads": [
{
"name": "agent-fly-a",
"base64": "YWdlbnQgc3RhdGUgYnl0ZXM="
},
{
"name": "executor-fly-a",
"base64": "ZXhlY3V0b3Igc3RhdGU="
},
{
"name": "task-ledger",
"base64": "eyJyYW5rIjoxMH0="
},
{
"name": "prior-inspection",
"base64": "eyJtYXAiOjQwfQ=="
},
{
"name": "world",
"base64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}
],
"envelope": {
"base64": "RkxZU0VTUzEBAAAAIAAAAAAIAAAFAAAAIAgAAAAAAAB7ImFnZW50cyI6W3siYWdlbnRJZCI6ImZseS1hIiwiYnJhaW5UaWNrcyI6IjI1MzQiLCJkYXRhc2V0RGlnZXN0IjoiNmMwYWYxZjA3ODRlZjYzYTM5M2VlNzdkNjE0ZTgyNDZjNjI1MDUxMzYwZjNmMWE0ODgzODM3NGM1ZDM1NWI1MiIsIm1vZGVsVmVyc2lvbiI6ImxpZi0xbXMtZjY0LXYyIiwicGF5bG9hZCI6ImFnZW50LWZseS1hIiwicGxhc3RpY2l0eVZlcnNpb24iOiJmbHkta2MtbWJvbi1yc3RkcC12MiIsInByb2ZpbGVEaWdlc3QiOiIxOTAwZWFiNmMwMjg0ODNkNzEyNjU5OWVlNmY1MGRlMGQyNzkwN2I1YzY1ZmE5MDUyNDU4MGI0YjBmOTg1MmIwIiwicmVtYWluZGVyIjp7ImRlbm9taW5hdG9yIjoiMyIsIm51bWVyYXRvciI6IjEwMDAwMDAifSwic2VlZCI6LTE4NDk0NjA2M31dLCJjaGVja3BvaW50SWQiOiJja3B0LTEiLCJjb21wYXRpYmlsaXR5Ijp7ImJhY2tlbmREaWdlc3QiOiIxMGUwOGE0MTllODUwZWJhMWViYmExOGZkZDI4ZWI3ZWMxYjdlOGJhYTliY2MzYjk3M2UyYjg4OTFlYzcyNmJlIiwiY29udGVudERpZ2VzdCI6ImVkNzAwMmI0MzllOWFjODQ1ZjIyMzU3ZDgyMmJhYzE0NDQ3MzBmYmRiNjAxNmQzZWM5NDMyMjk3YjllYzlmNzMiLCJjb250cm9sbGVyRGlnZXN0IjoiYzE0NzIxMzViMTRjNzdjOGJlZjk4ZTczZjcwMjA4MzI1ZmEwZGNmMWU2YmQ2NjhhZTliMzFhOWNlYTI5NWZlNyIsInBhcnNlckRpZ2VzdCI6ImIxN2Q0NTEyMTE1MDkyOGYyMTQ2YWY0OWUxOTVlZmYxZWVmNWQ2NzMyNWJlMjczYTczM2ZiNzRhY2FkYWEzNDIiLCJwYXRjaERpZ2VzdCI6ImE0ODk1ZWI0NGFmYzMzNmZlY2JiYTZlNTIwY2Q2N2UxNzhkYWNlMDI3NjY1NWQxMDJmY2VmZmE4ZTVmNzA1NzAiLCJzdGF0ZUZvcm1hdElkIjoiZmx5c2Vzcy0xIn0sImNvbXBvc2l0aW9uRGlnZXN0IjoiNzMwZDcyNWM4YTU5ZDNhNzMwM2RlZjJiZWQwNDFhNTc3ZWRiNDI1NWFhYmQ0ODg5Y2UxMjkxODMxMWQ5NTJmMCIsImNvb3JkaW5hdG9yIjp7ImFkbWlzc2lvblN0YXRlIjpudWxsLCJldmVudFdhdGVybWFya3MiOnsibGFzdEV2ZW50SWQiOiJldnQtMSIsImxhc3RPcmRpbmFsIjoiNyJ9LCJleGVjdXRvclN0YXRlIjpbeyJhZ2VudElkIjoiZmx5LWEiLCJwYXlsb2FkIjoiZXhlY3V0b3ItZmx5LWEifV0sInByaW9ySW5zcGVjdGlvbiI6InByaW9yLWluc3BlY3Rpb24iLCJ0YXNrTGVkZ2VyIjoidGFzay1sZWRnZXIifSwiZW52ZWxvcGVWZXJzaW9uIjoxLCJlcGlzb2RlSWQiOiJlcGlzb2RlLTEiLCJoZWxwZXJTdGF0ZSI6W10sInBheWxvYWRzIjpbeyJieXRlTGVuZ3RoIjoiMTciLCJkaWdlc3QiOiIxMzIxZGZmYjBjZGM2ZjkwOTJjYmY3ZmEyYTVmYzY4YmJlZDEyYzk5M2Q1YWQzOTgyNjQwMTI4MTBjZTliZjkzIiwibmFtZSI6ImFnZW50LWZseS1hIn0seyJieXRlTGVuZ3RoIjoiMTQiLCJkaWdlc3QiOiIzYWVlNjBkZjdlMjllZmViYTdmNWY5OWZjNTg2NzY0N2IzNmFlYmZmMWQ1ZDNjODM4ZGJmZjMyMzEyMmU2NDYyIiwibmFtZSI6ImV4ZWN1dG9yLWZseS1hIn0seyJieXRlTGVuZ3RoIjoiMTEiLCJkaWdlc3QiOiI0MGIwMGVkMmJiYmE5MDFkNjgyMDVmZjcxYjA0YTQ0YjllZTUzYzUxY2IzMTA5YWEyY2VhYTQ0ZjFjNDU3MjdlIiwibmFtZSI6InRhc2stbGVkZ2VyIn0seyJieXRlTGVuZ3RoIjoiMTAiLCJkaWdlc3QiOiIyYzEzYjdiNGQ5YTk5MTY4MDFhYjkxOTFjMzE0ZjMxYjA0NWU5YjljNWI2NjlhNmMwNDc0ZjAyMTdlZjc1YmY1IiwibmFtZSI6InByaW9yLWluc3BlY3Rpb24ifSx7ImJ5dGVMZW5ndGgiOiI2NCIsImRpZ2VzdCI6ImY1YTVmZDQyZDE2YTIwMzAyNzk4ZWY2ZWQzMDk5NzliNDMwMDNkMjMyMGQ5ZjBlOGVhOTgzMWE5Mjc1OWZiNGIiLCJuYW1lIjoid29ybGQifV0sInBvcnRNYXAiOlt7ImFnZW50SWQiOiJmbHktYSIsInBvcnRJZCI6InBvcnQtMSJ9XSwic2NoZWR1bGVySWQiOiJsb2Nrc3RlcC12MSIsInNvdXJjZVNjb3BlIjp7ImVwb2NoIjoiZXBvY2gtMSIsInNlc3Npb25JZCI6ImRlbW8iLCJzdGVwIjoiNDIifSwid29ybGRUaW1lIjp7ImRlbm9taW5hdG9yIjoiMSIsIm51bWVyYXRvciI6IjcwMDAwMDAwMCJ9fWFnZW50LWZseS1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQCgAAAAAAABEAAAAAAAAAEyHf+wzcb5CSy/f6Kl/Gi77RLJk9WtOYJkASgQzpv5NleGVjdXRvci1mbHktYQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaAoAAAAAAAAOAAAAAAAAADruYN9+Ke/rp/X5n8WGdkezauv/HV08g42/8yMSLmRidGFzay1sZWRnZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgKAAAAAAAACwAAAAAAAABAsA7Su7qQHWggX/cbBKRLnuU8UcsxCaos6qRPHEVyfnByaW9yLWluc3BlY3Rpb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACICgAAAAAAAAoAAAAAAAAALBO3tNmpkWgBq5GRwxTzGwRem5xbZppsBHTwIX73W/V3b3JsZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmAoAAAAAAABAAAAAAAAAAPWl/ULRaiAwJ5jvbtMJl5tDAD0jINnw6OqYMaknWftLYWdlbnQgc3RhdGUgYnl0ZXMAAAAAAAAAZXhlY3V0b3Igc3RhdGUAAHsicmFuayI6MTB9AAAAAAB7Im1hcCI6NDB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgLAAAAAAAAq++fEx+FvDZho/eB4imbENN4HZrGNC2OCAsI7/gp9r5GTFlTRVNTRg==",
"byteLength": 2824,
"layout": {
"headerBytes": 32,
"manifestOffset": "32",
"manifestBytes": 2048,
"tableOffset": "2080",
"tableEntryBytes": 112,
"entries": [
{
"name": "agent-fly-a",
"offset": "2640",
"byteLength": "17",
"digest": "1321dffb0cdc6f9092cbf7fa2a5fc68bbed12c993d5ad398264012810ce9bf93"
},
{
"name": "executor-fly-a",
"offset": "2664",
"byteLength": "14",
"digest": "3aee60df7e29efeba7f5f99fc5867647b36aebff1d5d3c838dbff323122e6462"
},
{
"name": "task-ledger",
"offset": "2680",
"byteLength": "11",
"digest": "40b00ed2bbba901d68205ff71b04a44b9ee53c51cb3109aa2ceaa44f1c45727e"
},
{
"name": "prior-inspection",
"offset": "2696",
"byteLength": "10",
"digest": "2c13b7b4d9a9916801ab9191c314f31b045e9b9c5b669a6c0474f0217ef75bf5"
},
{
"name": "world",
"offset": "2712",
"byteLength": "64",
"digest": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
}
],
"footerOffset": "2776",
"footerBytes": 48,
"totalBytes": "2824"
}
},
"corruption": [
{
"name": "a flipped magic byte",
"offset": 0,
"reason": "wrong magic"
},
{
"name": "an unsupported version",
"offset": 8,
"reason": "unsupported version"
},
{
"name": "a flipped manifest byte",
"offset": 32,
"reason": "the footer digest covers the manifest"
},
{
"name": "a flipped payload byte",
"offset": 2640,
"reason": "every payload carries its own digest"
},
{
"name": "a flipped footer digest byte",
"offset": 2784,
"reason": "the footer digest must match the contents"
},
{
"name": "a flipped footer magic byte",
"offset": 2816,
"reason": "a truncated file cannot look complete"
}
]
}

View file

@ -0,0 +1,9 @@
{
"description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.",
"contractDigest": "7932aef30c4d2d16e428081affc4e0ad187987f5b361138d553e54fd7f843b50",
"schemaSetVersion": 1,
"schemaSetBytes": 26685,
"types": 53,
"enums": 11,
"limits": 25
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
{
"description": "Boundary cases both languages build from a recipe, because the payload is too large to store.",
"padSchema": {
"id": "pad.v1",
"version": 1,
"digest": "018689202f154300eeda48ccee8cc021c36672df03f7404097613f5898fdfdcc"
},
"cases": [
{
"name": "typed value exactly at the 32 KiB cap",
"kind": "padded-typed-value",
"padCharacters": 32635,
"expect": "accept",
"reason": "32768 bytes of canonical JSON is the limit, not one byte less"
},
{
"name": "typed value one byte over the cap",
"kind": "padded-typed-value",
"padCharacters": 32636,
"expect": "reject",
"reason": "a TypedValue is at most 32 KiB of canonical JSON"
},
{
"name": "request that fills the 64 KiB envelope exactly",
"kind": "padded-request",
"padCharacters": 60000,
"expect": "accept",
"reason": "65536 bytes including the envelope wrapper is admissible",
"envelopeTotal": 65536
},
{
"name": "request one byte past the envelope ceiling",
"kind": "padded-request",
"padCharacters": 60000,
"expect": "reject",
"reason": "the complete envelope must fit Flybus's 64-KiB maximum",
"envelopeTotal": 65537
},
{
"name": "error message of 512 code points",
"kind": "error-message",
"codePoints": 512,
"expect": "accept",
"reason": "messages are <= 512 code points"
},
{
"name": "error message of 513 code points",
"kind": "error-message",
"codePoints": 513,
"expect": "reject",
"reason": "messages are <= 512 code points"
},
{
"name": "error message of 512 astral code points",
"kind": "error-message-astral",
"codePoints": 512,
"expect": "accept",
"reason": "the bound counts code points, not UTF-16 units or bytes"
},
{
"name": "error message of 513 astral code points",
"kind": "error-message-astral",
"codePoints": 513,
"expect": "reject",
"reason": "the bound counts code points, not UTF-16 units or bytes"
}
],
"recipes": {
"padded-typed-value": "a TypedValue whose schema is padSchema and whose value is {\"pad\": <padCharacters> 'a' characters}; validate it",
"padded-request": "a SessionRpcRequest req-1 with scope null and params {\"pad\": <padCharacters> 'a' characters}; canonicalize it, then require the envelope to fit with an overhead of envelopeTotal minus that canonical length",
"error-message": "a SessionRpcFailure with code INTERNAL, mutation unknown and a message of <codePoints> 'x' characters",
"error-message-astral": "the same failure with a message of <codePoints> repetitions of U+10400, one code point and two UTF-16 units each"
}
}

View file

@ -0,0 +1,111 @@
{
"description": "Bus callId, domain requestId, artifact identity and delivery/hold owner tokens are four types, not four spellings of one string.",
"cases": [
{
"text": "call-0",
"busCallId": true,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "call-102",
"busCallId": true,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "req-41",
"busCallId": false,
"domainRequestId": true,
"ownerToken": null
},
{
"text": "dlv-7",
"busCallId": false,
"domainRequestId": false,
"ownerToken": "delivery"
},
{
"text": "own-9",
"busCallId": false,
"domainRequestId": false,
"ownerToken": "hold"
},
{
"text": "req-041",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "call-",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "call",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "callid-1",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "request-1",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "REQ-1",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "dlv-07",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "sub-1",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
},
{
"text": "svc-1",
"busCallId": false,
"domainRequestId": false,
"ownerToken": null
}
],
"artifact": {
"ref": {
"storeId": "store-1",
"artifactId": "frame-1",
"generation": "1",
"byteLength": "92160",
"contentType": "image/x-rgba8",
"digest": null
},
"identity": {
"storeId": "store-1",
"artifactId": "frame-1",
"generation": "1"
},
"reason": "the identity is the naming half of an ArtifactRef; byteLength, contentType and digest are not identity, and an AssetRef is not an artifact at all"
},
"asset": {
"id": "profile-fly-a",
"digest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"byteLength": "4096",
"format": "flyprofile"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,667 @@
{
"description": "ipc-v1 section 5: the operation key of a step mutation and the canonical body a duplicate is compared against.",
"keys": [
{
"name": "Agent.Prepare on fly-a at step 41",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"method": "Agent.Prepare",
"workerId": "fly-a",
"digest": "eda73d62bb5d616052997ab5e9d2c4ba5872fdefb44cf706910a18a24d39225f"
},
{
"name": "Agent.Prepare on fly-b at step 41",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"method": "Agent.Prepare",
"workerId": "fly-b",
"digest": "5a7fd15da236f3fd6343698a8cc5c4f14fa7d84683a819d5a28904d90f700c4e"
},
{
"name": "Agent.Commit on fly-a at step 41",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"method": "Agent.Commit",
"workerId": "fly-a",
"digest": "2a89a120e5594895d024b4ffc2a7d225bd27322cfd345ebd78e80ed7b271c680"
},
{
"name": "Agent.Prepare on fly-a at step 42",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "42"
},
"method": "Agent.Prepare",
"workerId": "fly-a",
"digest": "5bb184cab6c3c2d7567a7979d860a7e9b26ea52bdc92b139053b0a2f81a9ed4d"
},
{
"name": "Agent.Prepare on fly-a in another epoch",
"scope": {
"sessionId": "demo",
"epoch": "epoch-2",
"step": "41"
},
"method": "Agent.Prepare",
"workerId": "fly-a",
"digest": "fa1d9cf3936aaffe3cedd0e586d7763efca2d250e89dd85b849cf35fbd91a477"
},
{
"name": "Environment.Advance at step 41",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"method": "Environment.Advance",
"workerId": "world",
"digest": "c89c390aa78ef2895d9e2b86a6000160923dc54e3be75529e0289cd18b9088b1"
}
],
"bodies": [
{
"name": "Agent.Prepare body",
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
]
},
"digest": "74c9d5f27b2cb06922cc3ca13d87c67faea0a6c73aefbbdb21ba8f52c6d33f60"
},
{
"name": "Environment.Advance body",
"method": "Environment.Advance",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"batchId": "batch-41",
"controls": [
{
"portId": "port-1",
"buttons": [
{
"id": "a",
"down": true
},
{
"id": "b",
"down": false
},
{
"id": "start",
"down": false
},
{
"id": "select",
"down": false
},
{
"id": "up",
"down": false
},
{
"id": "down",
"down": false
},
{
"id": "left",
"down": false
},
{
"id": "right",
"down": false
}
],
"axes": [
{
"id": "stick-x",
"value": 0.0
},
{
"id": "trigger",
"value": 0.0
}
]
}
]
},
"digest": "5aaae9c083336460a2ce34fb50e191f7108444a953f09b952e97a48b7962ee79"
},
{
"name": "Worker.Hello body with no scope",
"method": "Worker.Hello",
"scope": null,
"params": {
"sessionId": "demo",
"expectedWorkerId": "fly-a",
"role": "agent",
"supportedMajors": [
1
]
},
"digest": "0c07677a45178d8680dddbeec56d4e09f6ccbfe5fe77d9d4ff2fb07b369febaf"
}
],
"pairs": [
{
"name": "the same operation retried on a new bus call",
"left": {
"method": "Environment.Advance",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"batchId": "batch-41",
"controls": [
{
"portId": "port-1",
"buttons": [
{
"id": "a",
"down": true
},
{
"id": "b",
"down": false
},
{
"id": "start",
"down": false
},
{
"id": "select",
"down": false
},
{
"id": "up",
"down": false
},
{
"id": "down",
"down": false
},
{
"id": "left",
"down": false
},
{
"id": "right",
"down": false
}
],
"axes": [
{
"id": "stick-x",
"value": 0.0
},
{
"id": "trigger",
"value": 0.0
}
]
}
]
}
},
"right": {
"method": "Environment.Advance",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"batchId": "batch-41",
"controls": [
{
"portId": "port-1",
"buttons": [
{
"id": "a",
"down": true
},
{
"id": "b",
"down": false
},
{
"id": "start",
"down": false
},
{
"id": "select",
"down": false
},
{
"id": "up",
"down": false
},
{
"id": "down",
"down": false
},
{
"id": "left",
"down": false
},
{
"id": "right",
"down": false
}
],
"axes": [
{
"id": "stick-x",
"value": 0.0
},
{
"id": "trigger",
"value": 0.0
}
]
}
]
}
},
"workerId": "world",
"sameKey": true,
"sameBody": true,
"reason": "a retry reuses the requestId and body; only the callId changes, and the callId is not in either digest"
},
{
"name": "the same batch id with altered controls",
"left": {
"method": "Environment.Advance",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"batchId": "batch-41",
"controls": [
{
"portId": "port-1",
"buttons": [
{
"id": "a",
"down": true
},
{
"id": "b",
"down": false
},
{
"id": "start",
"down": false
},
{
"id": "select",
"down": false
},
{
"id": "up",
"down": false
},
{
"id": "down",
"down": false
},
{
"id": "left",
"down": false
},
{
"id": "right",
"down": false
}
],
"axes": [
{
"id": "stick-x",
"value": 0.0
},
{
"id": "trigger",
"value": 0.0
}
]
}
]
}
},
"right": {
"method": "Environment.Advance",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"batchId": "batch-41",
"controls": [
{
"portId": "port-1",
"buttons": [
{
"id": "a",
"down": true
},
{
"id": "b",
"down": false
},
{
"id": "start",
"down": false
},
{
"id": "select",
"down": false
},
{
"id": "up",
"down": false
},
{
"id": "down",
"down": false
},
{
"id": "left",
"down": false
},
{
"id": "right",
"down": false
}
],
"axes": [
{
"id": "stick-x",
"value": 1.0
},
{
"id": "trigger",
"value": 0.0
}
]
}
]
}
},
"workerId": "world",
"sameKey": true,
"sameBody": false,
"reason": "same key, changed body: CONFLICT, never a second world mutation"
},
{
"name": "params written with their keys in another order",
"left": {
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
]
}
},
"right": {
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
],
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"agentId": "fly-a"
}
},
"workerId": "fly-a",
"sameKey": true,
"sameBody": true,
"reason": "RFC 8785 sorts keys, so serialization order is not a body change"
},
{
"name": "the same body one step later",
"left": {
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
]
}
},
"right": {
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "42"
},
"params": {
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
]
}
},
"workerId": "fly-a",
"sameKey": false,
"sameBody": false,
"reason": "the step is part of both the key and the body"
},
{
"name": "the same body on another worker",
"left": {
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
]
}
},
"right": {
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"agentId": "fly-a",
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
"interval": {
"numerator": "50000000",
"denominator": "3"
},
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
"preStepStimulations": [
{
"id": "stim-1",
"kindId": "sugar",
"durationMs": 50.0
}
]
}
},
"workerId": "fly-a",
"rightWorkerId": "fly-b",
"sameKey": false,
"sameBody": true,
"reason": "the worker is part of the key, not of the body"
}
],
"rejected": [
{
"name": "a body carrying a bus callId",
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"callId": "call-2",
"agentId": "fly-a"
},
"reason": "the canonical body excludes bus callIds"
},
{
"name": "a body carrying a delivery id",
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"deliveryId": "dlv-7"
},
"reason": "the canonical body excludes deliveryIds"
},
{
"name": "a body carrying an owner token",
"method": "Agent.Commit",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"nextInput": {
"frame": {
"ownerId": "own-3"
}
}
},
"reason": "the canonical body excludes owner tokens"
},
{
"name": "a body carrying a pinned service incarnation",
"method": "Agent.Prepare",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {
"expectedIncarnation": "svc-1"
},
"reason": "route pinning is transport state, not domain state"
},
{
"name": "an empty method",
"method": "",
"scope": {
"sessionId": "demo",
"epoch": "epoch-1",
"step": "41"
},
"params": {},
"reason": "methods are 1..=128 printable ASCII characters"
}
]
}

View file

@ -0,0 +1,249 @@
{
"description": "Checked rational arithmetic and the step-v1 section 5 tick accumulator.",
"accumulator": [
{
"name": "a synthetic 60 Hz environment on a 1 ms model tick",
"stepDuration": {
"numerator": "50000000",
"denominator": "3"
},
"tickDuration": {
"numerator": "1000000",
"denominator": "1"
},
"steps": [
{
"ticks": "16",
"remainder": {
"numerator": "2000000",
"denominator": "3"
}
},
{
"ticks": "17",
"remainder": {
"numerator": "1000000",
"denominator": "3"
}
},
{
"ticks": "17",
"remainder": {
"numerator": "0",
"denominator": "1"
}
}
],
"totalTicks": "50",
"reason": "16, 17, 17 and a remainder of zero after three steps"
},
{
"name": "a whole millisecond cadence never accumulates a remainder",
"stepDuration": {
"numerator": "16000000",
"denominator": "1"
},
"tickDuration": {
"numerator": "1000000",
"denominator": "1"
},
"steps": [
{
"ticks": "16",
"remainder": {
"numerator": "0",
"denominator": "1"
}
},
{
"ticks": "16",
"remainder": {
"numerator": "0",
"denominator": "1"
}
}
],
"totalTicks": "32",
"reason": ""
},
{
"name": "a step shorter than one tick advances nothing and keeps the remainder",
"stepDuration": {
"numerator": "500000",
"denominator": "1"
},
"tickDuration": {
"numerator": "1000000",
"denominator": "1"
},
"steps": [
{
"ticks": "0",
"remainder": {
"numerator": "500000",
"denominator": "1"
}
},
{
"ticks": "1",
"remainder": {
"numerator": "0",
"denominator": "1"
}
}
],
"totalTicks": "1",
"reason": "the fractional period is carried, not rounded"
}
],
"add": [
{
"a": {
"numerator": "0",
"denominator": "1"
},
"b": {
"numerator": "50000000",
"denominator": "3"
},
"sum": {
"numerator": "50000000",
"denominator": "3"
}
},
{
"a": {
"numerator": "1",
"denominator": "3"
},
"b": {
"numerator": "1",
"denominator": "6"
},
"sum": {
"numerator": "1",
"denominator": "2"
}
},
{
"a": {
"numerator": "18446744073709551615",
"denominator": "1"
},
"b": {
"numerator": "1",
"denominator": "2"
},
"error": "reduced value does not fit U64"
}
],
"subtract": [
{
"a": {
"numerator": "50000000",
"denominator": "3"
},
"b": {
"numerator": "50000000",
"denominator": "3"
},
"difference": {
"numerator": "0",
"denominator": "1"
}
},
{
"a": {
"numerator": "1",
"denominator": "2"
},
"b": {
"numerator": "1",
"denominator": "3"
},
"difference": {
"numerator": "1",
"denominator": "6"
}
},
{
"a": {
"numerator": "0",
"denominator": "1"
},
"b": {
"numerator": "1",
"denominator": "2"
},
"error": "subtraction would be negative"
}
],
"multiply": [
{
"a": {
"numerator": "1000000",
"denominator": "1"
},
"k": "17",
"product": {
"numerator": "17000000",
"denominator": "1"
}
},
{
"a": {
"numerator": "0",
"denominator": "1"
},
"k": "1000",
"product": {
"numerator": "0",
"denominator": "1"
}
},
{
"a": {
"numerator": "18446744073709551615",
"denominator": "1"
},
"k": "2",
"error": "reduced value does not fit U64"
}
],
"compare": [
{
"a": {
"numerator": "0",
"denominator": "1"
},
"b": {
"numerator": "1000000",
"denominator": "1"
},
"ordering": "less"
},
{
"a": {
"numerator": "1",
"denominator": "3"
},
"b": {
"numerator": "1",
"denominator": "3"
},
"ordering": "equal",
"reason": "equal values compare equal; an unreduced 2/6 never reaches a comparison, because it never parses"
},
{
"a": {
"numerator": "50000000",
"denominator": "3"
},
"b": {
"numerator": "1000000",
"denominator": "1"
},
"ordering": "greater"
}
]
}

View file

@ -0,0 +1,77 @@
{
"description": "Byte sequences every implementation must refuse before validation.",
"cases": [
{
"name": "duplicate key at the top level",
"type": "Scope",
"base64": "eyJzZXNzaW9uSWQiOiJkZW1vIiwiZXBvY2giOiJlcG9jaC0xIiwic3RlcCI6IjEiLCJzdGVwIjoiMiJ9",
"reason": "duplicate JSON keys are refused at any depth"
},
{
"name": "duplicate key inside a nested object",
"type": "TypedValue",
"base64": "eyJzY2hlbWEiOnsiaWQiOiJhLnYxIiwidmVyc2lvbiI6MSwiZGlnZXN0IjoiYmJhNzk1MWMwNDY2NGNkNDliYjZlZWQxZGE2ZGMxYTRlMTdlOTg5ZTc4N2JmMmU3MmY1OTMzYTVjNjE3MTM3MiJ9LCJ2YWx1ZSI6eyJ4IjoxLCJ4IjoyfX0=",
"reason": "duplicate JSON keys are refused at any depth"
},
{
"name": "invalid UTF-8 in a string",
"type": "Scope",
"base64": "eyJzZXNzaW9uSWQiOiJkZf9tbyIsImVwb2NoIjoiZXBvY2gtMSIsInN0ZXAiOiIxIn0=",
"reason": "the envelope is UTF-8"
},
{
"name": "invalid UTF-8 in a key",
"type": "Scope",
"base64": "eyJzZXNzaW9u/0lkIjoiZGVtbyIsImVwb2NoIjoiZXBvY2gtMSIsInN0ZXAiOiIxIn0=",
"reason": "the envelope is UTF-8"
},
{
"name": "NaN literal",
"type": "Stimulus",
"base64": "eyJpZCI6InN0aW0tMSIsImtpbmRJZCI6InN1Z2FyIiwiZHVyYXRpb25NcyI6TmFOfQ==",
"reason": "NaN and Infinity are not JSON"
},
{
"name": "Infinity literal",
"type": "Stimulus",
"base64": "eyJpZCI6InN0aW0tMSIsImtpbmRJZCI6InN1Z2FyIiwiZHVyYXRpb25NcyI6SW5maW5pdHl9",
"reason": "NaN and Infinity are not JSON"
},
{
"name": "number that overflows a double",
"type": "Stimulus",
"base64": "eyJpZCI6InN0aW0tMSIsImtpbmRJZCI6InN1Z2FyIiwiZHVyYXRpb25NcyI6MWU5OTl9",
"reason": "a non-finite number never survives parsing"
},
{
"name": "integer past the exact double range",
"type": "Stimulus",
"base64": "eyJpZCI6InN0aW0tMSIsImtpbmRJZCI6InN1Z2FyIiwiZHVyYXRpb25NcyI6OTAwNzE5OTI1NDc0MDk5M30=",
"reason": "canonical JSON cannot encode it exactly; counters are U64 strings"
},
{
"name": "trailing data after the object",
"type": "Scope",
"base64": "eyJzZXNzaW9uSWQiOiJkZW1vIiwiZXBvY2giOiJlcG9jaC0xIiwic3RlcCI6IjEifSB7fQ==",
"reason": "one value per payload"
},
{
"name": "truncated object",
"type": "Scope",
"base64": "eyJzZXNzaW9uSWQiOiJkZW1vIiwiZXBvY2giOiJlcG9jaC0xIg==",
"reason": "partial JSON is refused"
},
{
"name": "empty payload",
"type": "Scope",
"base64": "",
"reason": "an empty payload is not a JSON object"
},
{
"name": "a bare array",
"type": "Scope",
"base64": "W10=",
"reason": "a payload is an object"
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,185 @@
{
"description": "seed-derivation-v1 test vectors. Both languages must reproduce every seed.",
"algorithm": "seed-derivation-v1",
"prefix": "flybrain/seed-derivation-v1",
"materialTemplate": "<prefix>\\n<masterSeed>\\n<agentId>\\n",
"rule": "SHA-256 of the material, read as eight big-endian u32 lanes; the first nonzero lane is the seed as a two's-complement i32.",
"vectors": [
{
"masterSeed": "0",
"agentId": "fly-a",
"material": "flybrain/seed-derivation-v1\n0\nfly-a\n",
"materialDigest": "6cf7c34a422f4cdd890c64d0ef5a072e4f6ddc2ee18e83ee24a5f2214f4960d1",
"seed": 1828176714
},
{
"masterSeed": "0",
"agentId": "fly-b",
"material": "flybrain/seed-derivation-v1\n0\nfly-b\n",
"materialDigest": "48a52f4069cfca001ba5ee6ae870dccb5a5bd59d61babf333a01749b6a8f4025",
"seed": 1218785088
},
{
"masterSeed": "0",
"agentId": "fly-c",
"material": "flybrain/seed-derivation-v1\n0\nfly-c\n",
"materialDigest": "53a83c2d0cfce5a3356aab3b0b73a270201a2dff7816168a4167c71a32d863fe",
"seed": 1403534381
},
{
"masterSeed": "0",
"agentId": "fly-d",
"material": "flybrain/seed-derivation-v1\n0\nfly-d\n",
"materialDigest": "f202513f32fe6070eee65ad72027490f9218b86dc8cf74e4e3563f02d532ea94",
"seed": -234729153
},
{
"masterSeed": "1",
"agentId": "fly-a",
"material": "flybrain/seed-derivation-v1\n1\nfly-a\n",
"materialDigest": "6eab9d6d6d002ffc0e2cdf2d64dd8226f90c391e45641512d14edbb244225698",
"seed": 1856740717
},
{
"masterSeed": "1",
"agentId": "fly-b",
"material": "flybrain/seed-derivation-v1\n1\nfly-b\n",
"materialDigest": "00115cb341d839490c5485462fd7eedd8b6baeda3748054fcba0d1558e471a6c",
"seed": 1137843
},
{
"masterSeed": "1",
"agentId": "fly-c",
"material": "flybrain/seed-derivation-v1\n1\nfly-c\n",
"materialDigest": "af97482074d6d36e5f5920aaa1d87f02365045f3712300dbd243584f2cb4a64c",
"seed": -1349040096
},
{
"masterSeed": "1",
"agentId": "fly-d",
"material": "flybrain/seed-derivation-v1\n1\nfly-d\n",
"materialDigest": "1081b24880040dcc4d442f88451513e4fbcf8dae9ccb79329dd289fabbc7938b",
"seed": 276935240
},
{
"masterSeed": "42",
"agentId": "fly-a",
"material": "flybrain/seed-derivation-v1\n42\nfly-a\n",
"materialDigest": "f4f9f271d53e94c38c6260b99840513f9eb70e5bb442d63345e217185e6ba9f6",
"seed": -184946063
},
{
"masterSeed": "42",
"agentId": "fly-b",
"material": "flybrain/seed-derivation-v1\n42\nfly-b\n",
"materialDigest": "1feb285e834138ec823ffea696fbe5fb3c211f349ce17140e25286d06c56ece8",
"seed": 535504990
},
{
"masterSeed": "42",
"agentId": "fly-c",
"material": "flybrain/seed-derivation-v1\n42\nfly-c\n",
"materialDigest": "b5a251ed4521f91eab1d8a6813880794c5c6706eafc137d8ebc169f743c60eed",
"seed": -1247653395
},
{
"masterSeed": "42",
"agentId": "fly-d",
"material": "flybrain/seed-derivation-v1\n42\nfly-d\n",
"materialDigest": "e36d6e40f27a4ffe473351d4bd01154da2efb3c3038d88f3aa48f77963d465c6",
"seed": -479367616
},
{
"masterSeed": "9223372036854775808",
"agentId": "fly-a",
"material": "flybrain/seed-derivation-v1\n9223372036854775808\nfly-a\n",
"materialDigest": "d59f513b6fdce1646ed13907302e0cb3ca4696738dd7df38e9581397508ec933",
"seed": -710979269
},
{
"masterSeed": "9223372036854775808",
"agentId": "fly-b",
"material": "flybrain/seed-derivation-v1\n9223372036854775808\nfly-b\n",
"materialDigest": "c8de8ca0279829c438fa19c07f05768cfe050e8c21bdaa0fe3c68f8048b4e1a3",
"seed": -924939104
},
{
"masterSeed": "9223372036854775808",
"agentId": "fly-c",
"material": "flybrain/seed-derivation-v1\n9223372036854775808\nfly-c\n",
"materialDigest": "354ad1ec388aa7be6136151d0f6281cc17279b62e95d458c96f392aaac40ee62",
"seed": 894095852
},
{
"masterSeed": "9223372036854775808",
"agentId": "fly-d",
"material": "flybrain/seed-derivation-v1\n9223372036854775808\nfly-d\n",
"materialDigest": "58e61deb4777b7702bdcd7edf5a5bcd0d51273699cc1051c7fbc9a5ffedd9e15",
"seed": 1491475947
},
{
"masterSeed": "18446744073709551615",
"agentId": "fly-a",
"material": "flybrain/seed-derivation-v1\n18446744073709551615\nfly-a\n",
"materialDigest": "f888eb95bcab276936fce5f72df3a0741e36da26722f43cc2a974932dfd42cd1",
"seed": -125244523
},
{
"masterSeed": "18446744073709551615",
"agentId": "fly-b",
"material": "flybrain/seed-derivation-v1\n18446744073709551615\nfly-b\n",
"materialDigest": "d343df049e8ef71617e9af7a321cf498d61083c50229f2d6852077670c86acbd",
"seed": -750526716
},
{
"masterSeed": "18446744073709551615",
"agentId": "fly-c",
"material": "flybrain/seed-derivation-v1\n18446744073709551615\nfly-c\n",
"materialDigest": "88016b220245b431a4eb510d31b643475bb2496130b65ae81b165acd5a899292",
"seed": -2013172958
},
{
"masterSeed": "18446744073709551615",
"agentId": "fly-d",
"material": "flybrain/seed-derivation-v1\n18446744073709551615\nfly-d\n",
"materialDigest": "d9b5bee718c87599c9ef0b3c16f361dadf714a33f7dd1209a21108f4df700b16",
"seed": -642400537
}
],
"composition": {
"masterSeed": "42",
"agentIds": [
"fly-a",
"fly-b",
"fly-c",
"fly-d"
],
"seeds": [
-184946063,
535504990,
-1247653395,
-479367616
],
"reason": "independent per-agent seeds from one recorded master seed and stable agent ids"
},
"invalid": [
{
"masterSeed": "0",
"agentId": "Fly-A",
"reason": "an agent id is an Id: lowercase"
},
{
"masterSeed": "0",
"agentId": "",
"reason": "an agent id is 1..=64 characters"
},
{
"masterSeed": "0",
"agentIds": [
"fly-a",
"fly-a"
],
"reason": "a composition with a repeated agent id is refused rather than silently sharing a seed"
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,192 @@
//! Canonical JSON (RFC 8785) and the digest rules of ipc-v1 section 5.
use fly_session_types::scalar::{DomainType, Scope};
use fly_session_types::{canonical, fixtures};
use serde_json::{Value, json};
#[test]
fn object_keys_are_sorted_by_utf16_code_unit() {
let value = json!({"b": 1, "a": 2, "A": 3, "\u{00e9}": 4, "\u{10400}": 5, "\u{ff21}": 6});
assert_eq!(
canonical::canonicalize(&value).expect("canonicalizable"),
"{\"A\":3,\"a\":2,\"b\":1,\"\u{00e9}\":4,\"\u{10400}\":5,\"\u{ff21}\":6}",
"keys sort by UTF-16 code unit, so an astral key (leading surrogate D801) sorts \
before U+FF21, which is where a JavaScript string sort puts it too and where a sort \
by Unicode code point would not"
);
}
#[test]
fn numbers_print_the_way_ecmascript_prints_them() {
let file = fixtures::load("boundaries.json").expect("boundaries.json");
for case in file.get("doubles").and_then(Value::as_array).expect("doubles") {
let value = case.get("value").expect("value");
let accept = case.get("accept").and_then(Value::as_bool).expect("accept");
let outcome = canonical::canonicalize(value);
assert_eq!(
outcome.is_ok(),
accept,
"{value}: {}",
fixtures::field(case, "reason").unwrap_or("")
);
if let (Ok(text), Some(expected)) = (outcome, case.get("canonical").and_then(Value::as_str))
{
assert_eq!(text, expected, "{value} must print as {expected}");
}
}
}
#[test]
fn strings_are_escaped_the_way_json_stringify_escapes_them() {
let value = json!({"s": "quote \" backslash \\ tab \t newline \n bell \u{7} del \u{7f} e\u{301}"});
assert_eq!(
canonical::canonicalize(&value).expect("canonicalizable"),
"{\"s\":\"quote \\\" backslash \\\\ tab \\t newline \\n bell \\u0007 del \u{7f} e\u{301}\"}",
"only the escapes JSON.stringify emits, with lowercase hex"
);
}
#[test]
fn canonical_form_does_not_depend_on_the_input_formatting() {
let compact = br#"{"b":[1,2,{"y":true,"x":null}],"a":"z"}"#;
let pretty = br#"{
"a" : "z",
"b": [ 1, 2, { "x": null, "y": true } ]
}"#;
let left = canonical::parse_strict(compact).expect("parses");
let right = canonical::parse_strict(pretty).expect("parses");
assert_eq!(
canonical::canonicalize(&left).expect("canonicalizable"),
canonical::canonicalize(&right).expect("canonicalizable")
);
assert_eq!(
canonical::digest_of(&left).expect("digest"),
canonical::digest_of(&right).expect("digest"),
"whitespace and key order are not content"
);
}
#[test]
fn duplicate_keys_and_invalid_utf8_never_parse() {
assert!(canonical::parse_strict(br#"{"a":1,"a":2}"#).is_err());
assert!(canonical::parse_strict(b"{\"a\":\"\xff\"}").is_err());
assert!(canonical::parse_strict(br#"{"a":1} {"b":2}"#).is_err());
assert!(canonical::parse_strict(br#"{"a":NaN}"#).is_err());
}
#[test]
fn an_envelope_over_64_kib_is_refused() {
let big = json!({"pad": "a".repeat(canonical::MAX_ENVELOPE_BYTES)});
assert!(canonical::require_envelope_fit(&big, 0).is_err());
let small = json!({"pad": "a"});
let length = canonical::canonicalize(&small).expect("canonicalizable").len();
assert_eq!(
canonical::require_envelope_fit(&small, canonical::MAX_ENVELOPE_BYTES - length)
.expect("fits exactly"),
canonical::MAX_ENVELOPE_BYTES
);
assert!(
canonical::require_envelope_fit(&small, canonical::MAX_ENVELOPE_BYTES - length + 1)
.is_err(),
"one byte past the ceiling is refused"
);
}
#[test]
fn operation_keys_match_the_fixture_and_separate_the_operations_they_should() {
let file = fixtures::load("operations.json").expect("operations.json");
let mut digests: Vec<(String, String)> = Vec::new();
for case in file.get("keys").and_then(Value::as_array).expect("keys") {
let name = fixtures::field(case, "name").expect("name");
let scope = Scope::from_json(case.get("scope").expect("scope")).expect("scope");
let method = fixtures::field(case, "method").expect("method");
let worker = fixtures::field(case, "workerId").expect("workerId");
let key = canonical::OperationKey::new(scope, method, worker).expect("a key");
let digest = key.digest().expect("digest");
assert_eq!(
digest,
fixtures::field(case, "digest").expect("digest"),
"{name}: operation key digest must match the fixture"
);
digests.push((name.to_owned(), digest));
}
for (index, (name, digest)) in digests.iter().enumerate() {
for (other_name, other) in &digests[index + 1..] {
assert_ne!(
digest, other,
"{name} and {other_name} are different operations"
);
}
}
}
#[test]
fn canonical_bodies_match_the_fixture() {
let file = fixtures::load("operations.json").expect("operations.json");
for case in file.get("bodies").and_then(Value::as_array).expect("bodies") {
let name = fixtures::field(case, "name").expect("name");
let method = fixtures::field(case, "method").expect("method");
let scope = match case.get("scope") {
Some(Value::Null) | None => None,
Some(v) => Some(Scope::from_json(v).expect("scope")),
};
let params = case.get("params").expect("params");
assert_eq!(
canonical::body_digest(method, scope.as_ref(), params).expect("digest"),
fixtures::field(case, "digest").expect("digest"),
"{name}: canonical body digest must match the fixture"
);
}
}
/// The pairs that decide whether a duplicate is a safe replay or a CONFLICT.
#[test]
fn operation_pairs_agree_with_the_fixture_about_sameness() {
let file = fixtures::load("operations.json").expect("operations.json");
for case in file.get("pairs").and_then(Value::as_array).expect("pairs") {
let name = fixtures::field(case, "name").expect("name");
let reason = fixtures::field(case, "reason").expect("reason");
let worker = fixtures::field(case, "workerId").expect("workerId");
let right_worker = fixtures::field(case, "rightWorkerId").unwrap_or(worker);
let side = |key: &str, worker: &str| {
let value = case.get(key).expect("side");
let method = fixtures::field(value, "method").expect("method");
let scope = Scope::from_json(value.get("scope").expect("scope")).expect("scope");
let params = value.get("params").expect("params");
let key_digest = canonical::OperationKey::new(scope.clone(), method, worker)
.expect("a key")
.digest()
.expect("digest");
let body = canonical::body_digest(method, Some(&scope), params).expect("digest");
(key_digest, body)
};
let (left_key, left_body) = side("left", worker);
let (right_key, right_body) = side("right", right_worker);
assert_eq!(
left_key == right_key,
case.get("sameKey").and_then(Value::as_bool).expect("sameKey"),
"{name}: operation key sameness. {reason}"
);
assert_eq!(
left_body == right_body,
case.get("sameBody").and_then(Value::as_bool).expect("sameBody"),
"{name}: canonical body sameness. {reason}"
);
}
}
#[test]
fn a_domain_body_can_never_carry_a_bus_identity() {
let file = fixtures::load("operations.json").expect("operations.json");
for case in file.get("rejected").and_then(Value::as_array).expect("rejected") {
let name = fixtures::field(case, "name").expect("name");
let method = fixtures::field(case, "method").expect("method");
let scope = Scope::from_json(case.get("scope").expect("scope")).expect("scope");
let params = case.get("params").expect("params");
assert!(
canonical::body_digest(method, Some(&scope), params).is_err(),
"{name} must be refused: {}",
fixtures::field(case, "reason").unwrap_or("")
);
}
}

View file

@ -0,0 +1,165 @@
//! The `FLYSESS1` envelope layout: its fixture, its offsets and the corruptions it refuses.
use fly_session_types::{canonical, checkpoint, fixtures};
use serde_json::Value;
fn envelope_bytes(file: &Value) -> Vec<u8> {
fixtures::decode_base64(
file.get("envelope")
.and_then(|e| e.get("base64"))
.and_then(Value::as_str)
.expect("base64"),
)
.expect("base64")
}
#[test]
fn the_fixture_envelope_decodes_to_its_recorded_layout() {
let file = fixtures::load("checkpoint-envelope.json").expect("checkpoint-envelope.json");
let bytes = envelope_bytes(&file);
let envelope = checkpoint::decode(&bytes).expect("a valid envelope");
checkpoint::validate_manifest(&envelope).expect("a complete manifest");
let layout = file["envelope"]["layout"].clone();
assert_eq!(&bytes[0..8], checkpoint::MAGIC);
assert_eq!(
bytes.len().to_string(),
layout["totalBytes"].as_str().expect("totalBytes")
);
assert_eq!(
envelope.layout.table_offset.to_string(),
layout["tableOffset"].as_str().expect("tableOffset")
);
assert_eq!(
envelope.layout.manifest_bytes,
layout["manifestBytes"].as_u64().expect("manifestBytes") as u32
);
let entries = layout["entries"].as_array().expect("entries");
assert_eq!(envelope.layout.entries.len(), entries.len());
for (entry, recorded) in envelope.layout.entries.iter().zip(entries) {
assert_eq!(entry.name, recorded["name"].as_str().expect("name"));
assert_eq!(
entry.offset.to_string(),
recorded["offset"].as_str().expect("offset")
);
assert_eq!(
entry.byte_length.to_string(),
recorded["byteLength"].as_str().expect("byteLength")
);
assert_eq!(
checkpoint::hex(&entry.digest),
recorded["digest"].as_str().expect("digest")
);
assert_eq!(entry.offset % 8, 0, "payloads start on an eight-byte boundary");
}
for payload in file["payloads"].as_array().expect("payloads") {
let name = payload["name"].as_str().expect("name");
let expected = fixtures::decode_base64(payload["base64"].as_str().expect("base64"))
.expect("base64");
assert_eq!(
envelope.payload(name).expect("a payload"),
expected.as_slice(),
"payload {name} must come back byte for byte"
);
}
assert_eq!(
envelope.manifest,
file["manifest"],
"the manifest round trips as canonical JSON"
);
}
#[test]
fn every_recorded_corruption_is_refused() {
let file = fixtures::load("checkpoint-envelope.json").expect("checkpoint-envelope.json");
let bytes = envelope_bytes(&file);
for case in file["corruption"].as_array().expect("corruption") {
let name = case["name"].as_str().expect("name");
let offset = case["offset"].as_u64().expect("offset") as usize;
let mut corrupted = bytes.clone();
corrupted[offset] ^= 0x01;
assert!(
checkpoint::decode(&corrupted).is_err(),
"{name} must be refused: {}",
case["reason"].as_str().unwrap_or("")
);
}
let truncated = &bytes[..bytes.len() - 1];
assert!(
checkpoint::decode(truncated).is_err(),
"a truncated envelope must be refused"
);
assert!(
checkpoint::decode(&bytes[..8]).is_err(),
"a header alone is not an envelope"
);
}
#[test]
fn a_flysim01_envelope_is_not_read_as_a_session_checkpoint() {
// The historical envelope: magic, u32 manifest length, manifest, chunks, CRC32.
let mut legacy = Vec::new();
legacy.extend_from_slice(b"FLYSIM01");
let manifest = br#"{"schemaVersion":2,"chunks":[]}"#;
legacy.extend_from_slice(&(manifest.len() as u32).to_le_bytes());
legacy.extend_from_slice(manifest);
legacy.extend_from_slice(&0u32.to_le_bytes());
assert!(
checkpoint::decode(&legacy).is_err(),
"FLYSESS1 is a new format; the old reader stays separate"
);
}
#[test]
fn the_layout_is_deterministic_and_the_manifest_is_canonical() {
let manifest = canonical::parse_strict(br#"{"b":2,"a":1}"#).expect("parses");
let payloads = vec![
("one".to_owned(), b"first".to_vec()),
("two".to_owned(), vec![0u8; 9]),
];
let bytes = checkpoint::encode(&manifest, &payloads).expect("encode");
let again = checkpoint::encode(&manifest, &payloads).expect("encode");
assert_eq!(bytes, again, "the same inputs produce the same bytes");
let envelope = checkpoint::decode(&bytes).expect("decode");
let start = checkpoint::HEADER_BYTES;
let end = start + envelope.layout.manifest_bytes as usize;
assert_eq!(
std::str::from_utf8(&bytes[start..end]).expect("utf-8"),
"{\"a\":1,\"b\":2}",
"the manifest is stored as canonical JSON"
);
assert_eq!(envelope.layout.entries[1].offset % 8, 0);
assert!(
checkpoint::encode(
&manifest,
&[("one".to_owned(), vec![]), ("one".to_owned(), vec![])]
)
.is_err(),
"payload names are unique"
);
assert!(
checkpoint::encode(&manifest, &[("One".to_owned(), vec![])]).is_err(),
"payload names are Ids, and the widening from letters-only is deliberate, not arbitrary"
);
assert!(
checkpoint::encode(&manifest, &[("a".to_owned(), Vec::new())]).is_ok(),
"an empty payload is still a payload"
);
}
#[test]
fn a_manifest_missing_a_required_field_is_not_a_complete_checkpoint() {
let file = fixtures::load("checkpoint-envelope.json").expect("checkpoint-envelope.json");
let full = file["manifest"].clone();
for field in checkpoint::REQUIRED_MANIFEST_FIELDS {
let mut manifest = full.clone();
manifest.as_object_mut().expect("object").remove(*field);
let bytes = checkpoint::encode(&manifest, &[]).expect("encode");
let envelope = checkpoint::decode(&bytes).expect("decode");
assert!(
checkpoint::validate_manifest(&envelope).is_err(),
"a manifest without {field:?} must be refused"
);
}
}

View file

@ -0,0 +1,133 @@
//! One place that knows how to read every type named by a fixture.
use flybus::wire::WireError;
use fly_session_types::media::*;
use fly_session_types::publishing::*;
use fly_session_types::rpc::*;
use fly_session_types::scalar::*;
use fly_session_types::trace::*;
use fly_session_types::workers::*;
use serde_json::Value;
/// Reads the value as `type_name`, re-runs its validate step and writes it back out.
///
/// Every fixture assertion goes through this, so a type that reads a field but forgets to
/// write it back fails the round trip.
pub fn round_trip(type_name: &str, value: &Value) -> std::result::Result<Value, WireError> {
macro_rules! arm {
($t:ty) => {
if type_name == <$t as DomainType>::TYPE_NAME {
let parsed = <$t as DomainType>::from_json(value)?;
parsed.validate()?;
return Ok(parsed.to_json());
}
};
}
arm!(Scope);
arm!(RationalNs);
arm!(SchemaRef);
arm!(TypedValue);
arm!(SessionRpcRequest);
arm!(SessionRpcSuccess);
arm!(SessionRpcFailure);
arm!(AssetRef);
arm!(SensoryInput);
arm!(Stimulus);
arm!(Reward);
arm!(AgentTelemetry);
arm!(AgentInitializeParams);
arm!(AgentInitializeResult);
arm!(PrepareParams);
arm!(PreparedDecision);
arm!(CommitParams);
arm!(AgentCommitResult);
arm!(ControllerSchema);
arm!(PortControl);
arm!(EnvironmentDescriptor);
arm!(EnvironmentInitializeParams);
arm!(EnvironmentInitializeResult);
arm!(WorldObservation);
arm!(AdvanceParams);
arm!(StepResult);
arm!(HelloParams);
arm!(HelloResult);
arm!(StatusResult);
arm!(AcknowledgeParams);
arm!(AcknowledgeResult);
arm!(ShutdownParams);
arm!(ShutdownResult);
arm!(TaskEvent);
arm!(EpisodeRequest);
arm!(ViewDescriptor);
arm!(ViewRef);
arm!(AudioDescriptor);
arm!(AudioRef);
arm!(CaptureParams);
arm!(CaptureResult);
arm!(StageRestoreParams);
arm!(StageRestoreResult);
arm!(ActivateRestoreParams);
arm!(ActivateRestoreResult);
arm!(SessionDescriptor);
arm!(CommittedSnapshot);
arm!(TraceBehaviour);
arm!(TraceOperational);
arm!(TransitionTrace);
Err(WireError(format!(
"no fixture reader for type {type_name:?}"
)))
}
/// The type names `round_trip` knows.
pub const READABLE_TYPES: &[&str] = &[
"Scope",
"RationalNs",
"SchemaRef",
"TypedValue",
"SessionRpcRequest",
"SessionRpcSuccess",
"SessionRpcFailure",
"AssetRef",
"SensoryInput",
"Stimulus",
"Reward",
"AgentTelemetry",
"AgentInitializeParams",
"AgentInitializeResult",
"PrepareParams",
"PreparedDecision",
"CommitParams",
"AgentCommitResult",
"ControllerSchema",
"PortControl",
"EnvironmentDescriptor",
"EnvironmentInitializeParams",
"EnvironmentInitializeResult",
"WorldObservation",
"AdvanceParams",
"StepResult",
"HelloParams",
"HelloResult",
"StatusResult",
"AcknowledgeParams",
"AcknowledgeResult",
"ShutdownParams",
"ShutdownResult",
"TaskEvent",
"EpisodeRequest",
"ViewDescriptor",
"ViewRef",
"AudioDescriptor",
"AudioRef",
"CaptureParams",
"CaptureResult",
"StageRestoreParams",
"StageRestoreResult",
"ActivateRestoreParams",
"ActivateRestoreResult",
"SessionDescriptor",
"CommittedSnapshot",
"TraceBehaviour",
"TraceOperational",
"TransitionTrace",
];

View file

@ -0,0 +1,78 @@
//! Rules that need a descriptor in hand: complete batches, the observation delay rule, byte
//! shapes and descriptor agreement.
use fly_session_types::fixtures;
use fly_session_types::publishing::{CommittedSnapshot, SessionDescriptor};
use fly_session_types::scalar::{DomainType, Result};
use fly_session_types::workers::{
EnvironmentDescriptor, PortControl, SensoryInput, StepResult, WorldObservation,
};
#[test]
fn every_descriptor_check_lands_the_way_the_fixture_says() {
let file = fixtures::load("descriptor-checks.json").expect("descriptor-checks.json");
let descriptor =
EnvironmentDescriptor::from_json(file.get("descriptor").expect("descriptor")).expect("descriptor");
let delayed = EnvironmentDescriptor::from_json(file.get("delayedDescriptor").expect("delayed"))
.expect("delayed descriptor");
let session = SessionDescriptor::from_json(file.get("sessionDescriptor").expect("session"))
.expect("session descriptor");
let previous = WorldObservation::from_json(file.get("stepResultPrevious").expect("previous"))
.expect("previous observation");
for case in fixtures::cases(&file).expect("cases") {
let name = fixtures::field(case, "name").expect("name");
let kind = fixtures::field(case, "kind").expect("kind");
let reason = fixtures::field(case, "reason").unwrap_or("");
let expect_accept = fixtures::field(case, "expect").expect("expect") == "accept";
let value = case.get("value").expect("value");
let outcome: Result<()> = match kind {
"portControl" => PortControl::from_json(value).and_then(|control| {
match descriptor.port(&control.port_id) {
Some(port) => control.validate_against(&port.controls),
None => fly_session_types::scalar::err("no such port"),
}
}),
"advanceControls" => value
.as_array()
.expect("an array of controls")
.iter()
.map(PortControl::from_json)
.collect::<Result<Vec<_>>>()
.and_then(|controls| descriptor.validate_batch(&controls)),
"sensoryInput" => SensoryInput::from_json(value)
.and_then(|input| input.validate_against(&descriptor.views)),
"sensoryInputDelayed" => {
SensoryInput::from_json(value).and_then(|input| input.validate_against(&delayed.views))
}
"worldObservation" => WorldObservation::from_json(value)
.and_then(|observation| observation.validate_against(&descriptor)),
"stepResult" => StepResult::from_json(value)
.and_then(|result| result.validate_against(&descriptor, &previous)),
"snapshot" => CommittedSnapshot::from_json(value)
.and_then(|snapshot| snapshot.validate_against(&session)),
other => panic!("unknown descriptor check kind {other:?}"),
};
assert_eq!(
outcome.is_ok(),
expect_accept,
"{name}: expected {}. {reason}. outcome: {outcome:?}",
if expect_accept { "accept" } else { "reject" }
);
}
}
/// The delay rule itself, stated once: `max(0, boundary - observationDelaySteps)`.
#[test]
fn the_required_producing_boundary_saturates_at_zero() {
let file = fixtures::load("descriptor-checks.json").expect("descriptor-checks.json");
let delayed = EnvironmentDescriptor::from_json(file.get("delayedDescriptor").expect("delayed"))
.expect("delayed descriptor");
let view = &delayed.views[0];
assert_eq!(view.observation_delay_steps, 2);
assert_eq!(view.required_produced_step(0), 0);
assert_eq!(view.required_produced_step(1), 0);
assert_eq!(view.required_produced_step(2), 0);
assert_eq!(view.required_produced_step(3), 1);
assert_eq!(view.frame_bytes(), u64::from(160u32 * 4 * 144));
}

View file

@ -0,0 +1,169 @@
//! The scalar encodings agree with the bus's, and the four identities cannot be confused.
use fly_session_types::fixtures;
use fly_session_types::scalar::{
ArtifactIdentity, BusCallId, DomainRequestId, OwnerKind, OwnerToken, is_digest, is_id, parse_u64,
};
use serde_json::Value;
/// The domain `Id`, `U64` and `Digest` are the bus encodings, not a second opinion about them.
#[test]
fn domain_scalars_are_the_bus_scalars() {
let ids = [
"a",
"fly-a",
"0",
"a.b_c-d",
"",
"A",
"-a",
".a",
"a b",
"fly/a",
&"a".repeat(64),
&"a".repeat(65),
];
for id in ids {
assert_eq!(
is_id(id),
flybus::wire::is_id(id),
"Id encoding must agree with the bus for {id:?}"
);
}
let numbers = [
"0",
"1",
"18446744073709551615",
"18446744073709551616",
"01",
"",
"-1",
"1.0",
" 1",
];
for text in numbers {
assert_eq!(
parse_u64(text),
flybus::wire::parse_u64(text),
"U64 encoding must agree with the bus for {text:?}"
);
}
let digests = [
&"a".repeat(64),
&"0".repeat(64),
&"A".repeat(64),
&"g".repeat(64),
&"a".repeat(63),
];
for digest in digests {
assert_eq!(
is_digest(digest),
flybus::wire::is_digest(digest),
"Digest encoding must agree with the bus"
);
}
}
#[test]
fn u64_boundaries_reject_from_the_fixture() {
let file = fixtures::load("boundaries.json").expect("boundaries.json");
let cases = file.get("u64").and_then(Value::as_array).expect("u64");
for case in cases {
let text = fixtures::field(case, "text").expect("text");
let accept = case.get("accept").and_then(Value::as_bool).expect("accept");
assert_eq!(
parse_u64(text).is_some(),
accept,
"{text:?}: {}",
fixtures::field(case, "reason").unwrap_or("")
);
}
}
/// bus callId, domain requestId and delivery/hold owner tokens are four types. The fixture
/// says, for every spelling, which of them accept it: no string is accepted by two.
#[test]
fn the_four_identities_never_accept_each_others_spellings() {
let file = fixtures::load("identities.json").expect("identities.json");
for case in fixtures::cases(&file).expect("cases") {
let text = fixtures::field(case, "text").expect("text");
let call = case.get("busCallId").and_then(Value::as_bool).expect("busCallId");
let request = case
.get("domainRequestId")
.and_then(Value::as_bool)
.expect("domainRequestId");
let owner = case.get("ownerToken").expect("ownerToken");
assert_eq!(BusCallId::parse(text).is_ok(), call, "busCallId {text:?}");
assert_eq!(
DomainRequestId::parse(text).is_ok(),
request,
"domainRequestId {text:?}"
);
match owner {
Value::Null => assert!(
OwnerToken::parse(text).is_err(),
"owner token {text:?} must be refused"
),
Value::String(kind) => {
let parsed = OwnerToken::parse(text).expect("an owner token");
let expected = match kind.as_str() {
"delivery" => OwnerKind::Delivery,
"hold" => OwnerKind::Hold,
other => panic!("unknown owner kind {other:?}"),
};
assert_eq!(parsed.kind(), expected, "owner kind of {text:?}");
}
other => panic!("unexpected ownerToken field {other:?}"),
}
let accepted = [call, request, OwnerToken::parse(text).is_ok()]
.iter()
.filter(|a| **a)
.count();
assert!(
accepted <= 1,
"{text:?} is accepted by more than one identity type"
);
}
}
/// An artifact identity is the naming half of a bus ArtifactRef, and nothing else in these
/// contracts is one: an AssetRef is persistent installed content, not live bytes.
#[test]
fn artifact_identity_is_the_naming_half_of_an_artifact_ref() {
let file = fixtures::load("identities.json").expect("identities.json");
let section = file.get("artifact").expect("artifact");
let reference =
flybus::wire::ArtifactRef::from_json(section.get("ref").expect("ref")).expect("a ref");
let identity = ArtifactIdentity::of(&reference);
identity.validate().expect("a valid identity");
let expected = section.get("identity").expect("identity");
assert_eq!(identity.store_id, expected["storeId"].as_str().unwrap());
assert_eq!(identity.artifact_id, expected["artifactId"].as_str().unwrap());
assert_eq!(identity.generation.to_string(), expected["generation"].as_str().unwrap());
let asset = fly_session_types::workers::AssetRef::from_json(section_asset(&file)).expect("asset");
assert_ne!(
asset.id, identity.artifact_id,
"the fixture's asset and artifact are deliberately different things"
);
}
fn section_asset(file: &Value) -> &Value {
file.get("asset").expect("asset")
}
/// A delivery id or hold token is connection-private. The domain reader has no field that
/// takes one, which is what `canonical::reject_bus_identities` enforces; here we only pin
/// that the two prefixes the bus issues are the two kinds this type knows.
#[test]
fn owner_tokens_come_in_exactly_two_kinds() {
assert_eq!(OwnerToken::delivery(7).as_str(), "dlv-7");
assert_eq!(OwnerToken::hold(9).as_str(), "own-9");
assert_eq!(OwnerToken::delivery(7).kind(), OwnerKind::Delivery);
assert_eq!(OwnerToken::hold(9).kind(), OwnerKind::Hold);
assert_eq!(BusCallId::from_serial(12).as_str(), "call-12");
assert_eq!(DomainRequestId::from_serial(41).as_str(), "req-41");
assert_eq!(DomainRequestId::from_serial(41).serial(), 41);
}
use fly_session_types::scalar::DomainType as _;

View file

@ -0,0 +1,177 @@
//! The `valid.json`, `invalid.json`, `raw.json` and `generated.json` fixtures.
mod common;
use fly_session_types::scalar::{DomainType, MAX_TYPED_VALUE_BYTES, SchemaRef, TypedValue};
use fly_session_types::{canonical, fixtures};
use serde_json::{Value, json};
use common::round_trip;
#[test]
fn every_valid_case_round_trips_and_canonicalizes_to_its_recorded_bytes() {
let file = fixtures::load("valid.json").expect("valid.json");
let cases = fixtures::cases(&file).expect("cases");
for case in cases {
let name = fixtures::field(case, "name").expect("name");
let type_name = fixtures::field(case, "type").expect("type");
let value = case.get("value").expect("value");
let written = round_trip(type_name, value)
.unwrap_or_else(|e| panic!("{name} ({type_name}) must be accepted: {e}"));
let canonical_in = canonical::canonicalize(value).expect("canonicalizable");
let canonical_out = canonical::canonicalize(&written).expect("canonicalizable");
assert_eq!(
canonical_in, canonical_out,
"{name}: reading and writing must preserve every field"
);
assert_eq!(
canonical_in,
fixtures::field(case, "canonical").expect("canonical"),
"{name}: canonical JSON must match the fixture"
);
assert_eq!(
canonical::sha256_hex(canonical_in.as_bytes()),
fixtures::field(case, "digest").expect("digest"),
"{name}: digest must match the fixture"
);
}
assert!(cases.len() >= 70, "the valid fixture should stay broad");
}
#[test]
fn every_type_the_readers_know_appears_in_the_valid_fixture() {
let file = fixtures::load("valid.json").expect("valid.json");
let cases = fixtures::cases(&file).expect("cases");
let covered: Vec<&str> = cases
.iter()
.map(|case| fixtures::field(case, "type").expect("type"))
.collect();
let missing: Vec<&&str> = common::READABLE_TYPES
.iter()
.filter(|t| !covered.contains(*t))
.collect();
assert!(
missing.is_empty(),
"every readable type needs at least one accepted fixture; missing {missing:?}"
);
}
#[test]
fn every_invalid_case_is_refused() {
let file = fixtures::load("invalid.json").expect("invalid.json");
let cases = fixtures::cases(&file).expect("cases");
for case in cases {
let name = fixtures::field(case, "name").expect("name");
let type_name = fixtures::field(case, "type").expect("type");
let reason = fixtures::field(case, "reason").expect("reason");
let value = case.get("value").expect("value");
let outcome = round_trip(type_name, value);
assert!(
outcome.is_err(),
"{name} ({type_name}) must be refused: {reason}"
);
}
assert!(cases.len() >= 80, "the invalid fixture should stay broad");
}
#[test]
fn every_raw_byte_case_is_refused_before_or_during_validation() {
let file = fixtures::load("raw.json").expect("raw.json");
for case in fixtures::cases(&file).expect("cases") {
let name = fixtures::field(case, "name").expect("name");
let type_name = fixtures::field(case, "type").expect("type");
let reason = fixtures::field(case, "reason").expect("reason");
let bytes = fixtures::base64(case, "base64").unwrap_or_default();
let outcome = canonical::parse_strict(&bytes).and_then(|value| {
round_trip(type_name, &value).map_err(|e| fly_session_types::scalar::wire_err(e.0))
});
assert!(outcome.is_err(), "{name} must be refused: {reason}");
}
}
/// The recipes in `generated.json`: payloads too large to store as fixtures.
#[test]
fn generated_boundary_cases_land_on_the_right_side_of_every_limit() {
let file = fixtures::load("generated.json").expect("generated.json");
let pad_schema = SchemaRef::from_json(file.get("padSchema").expect("padSchema")).expect("schema");
for case in fixtures::cases(&file).expect("cases") {
let name = fixtures::field(case, "name").expect("name");
let kind = fixtures::field(case, "kind").expect("kind");
let expect_accept = fixtures::field(case, "expect").expect("expect") == "accept";
let outcome: Result<(), String> = match kind {
"padded-typed-value" => {
let pad = case.get("padCharacters").and_then(Value::as_u64).expect("pad") as usize;
TypedValue::new(pad_schema.clone(), json!({"pad": "a".repeat(pad)}))
.map(|_| ())
.map_err(|e| e.0)
}
"padded-request" => {
let pad = case.get("padCharacters").and_then(Value::as_u64).expect("pad") as usize;
let total = case
.get("envelopeTotal")
.and_then(Value::as_u64)
.expect("envelopeTotal") as usize;
let request = json!({
"requestId": "req-1",
"scope": Value::Null,
"params": {"pad": "a".repeat(pad)},
});
let body = round_trip("SessionRpcRequest", &request).expect("a request");
let length = canonical::canonicalize(&body).expect("canonicalizable").len();
canonical::require_envelope_fit(&body, total - length)
.map(|_| ())
.map_err(|e| e.0)
}
"error-message" | "error-message-astral" => {
let points = case.get("codePoints").and_then(Value::as_u64).expect("codePoints")
as usize;
let character = if kind == "error-message" { 'x' } else { '\u{10400}' };
let message: String = std::iter::repeat_n(character, points).collect();
let failure = json!({
"type": "error",
"requestId": "req-41",
"workerId": "fly-a",
"incarnationId": "inc-1",
"scope": Value::Null,
"error": {"code": "INTERNAL", "message": message, "mutation": "unknown"},
});
round_trip("SessionRpcFailure", &failure)
.map(|_| ())
.map_err(|e| e.0)
}
other => panic!("unknown generated case kind {other:?}"),
};
assert_eq!(
outcome.is_ok(),
expect_accept,
"{name}: expected {}, got {outcome:?}",
if expect_accept { "accept" } else { "reject" }
);
}
}
#[test]
fn a_typed_value_at_the_cap_is_accepted_and_one_byte_more_is_not() {
let schema = SchemaRef::new("pad.v1", 1, &canonical::sha256_hex(b"pad.v1")).expect("schema");
let overhead = canonical::canonicalize(
&TypedValue::new(schema.clone(), json!({"pad": ""}))
.expect("empty")
.to_json(),
)
.expect("canonicalizable")
.len();
let at_cap = TypedValue::new(
schema.clone(),
json!({"pad": "a".repeat(MAX_TYPED_VALUE_BYTES - overhead)}),
)
.expect("exactly at the cap");
assert_eq!(at_cap.canonical_len().expect("length"), MAX_TYPED_VALUE_BYTES);
assert!(
TypedValue::new(
schema,
json!({"pad": "a".repeat(MAX_TYPED_VALUE_BYTES - overhead + 1)})
)
.is_err(),
"one byte over the cap must fail"
);
}

View file

@ -0,0 +1,140 @@
//! Checked rational arithmetic and the step-v1 section 5 tick accumulator.
use fly_session_types::scalar::{DomainType, RationalNs};
use fly_session_types::fixtures;
use serde_json::Value;
fn rational(value: &Value) -> RationalNs {
RationalNs::from_json(value).expect("a valid rational")
}
#[test]
fn the_accumulator_produces_the_fixture_tick_counts_and_remainders() {
let file = fixtures::load("rational.json").expect("rational.json");
for case in file
.get("accumulator")
.and_then(Value::as_array)
.expect("accumulator")
{
let name = fixtures::field(case, "name").expect("name");
let step = rational(case.get("stepDuration").expect("stepDuration"));
let tick = rational(case.get("tickDuration").expect("tickDuration"));
let mut accumulator = RationalNs::ZERO;
let mut total = 0u64;
for (index, expected) in case
.get("steps")
.and_then(Value::as_array)
.expect("steps")
.iter()
.enumerate()
{
accumulator = accumulator.checked_add(&step).expect("checked add");
let (ticks, remainder) = accumulator.divide_floor(&tick).expect("checked divide");
accumulator = remainder;
total += ticks;
assert_eq!(
ticks.to_string(),
fixtures::field(expected, "ticks").expect("ticks"),
"{name}: tick count at step {index}"
);
assert_eq!(
remainder,
rational(expected.get("remainder").expect("remainder")),
"{name}: remainder at step {index}"
);
assert!(
remainder < tick,
"{name}: the remainder is always less than one model tick"
);
}
assert_eq!(
total.to_string(),
fixtures::field(case, "totalTicks").expect("totalTicks"),
"{name}: total ticks"
);
}
}
#[test]
fn checked_arithmetic_reduces_or_refuses() {
let file = fixtures::load("rational.json").expect("rational.json");
for case in file.get("add").and_then(Value::as_array).expect("add") {
let outcome = rational(case.get("a").expect("a")).checked_add(&rational(case.get("b").expect("b")));
match case.get("sum") {
Some(sum) => assert_eq!(outcome.expect("a sum"), rational(sum)),
None => assert!(outcome.is_err(), "the sum must overflow: {case}"),
}
}
for case in file
.get("subtract")
.and_then(Value::as_array)
.expect("subtract")
{
let outcome =
rational(case.get("a").expect("a")).checked_sub(&rational(case.get("b").expect("b")));
match case.get("difference") {
Some(difference) => assert_eq!(outcome.expect("a difference"), rational(difference)),
None => assert!(outcome.is_err(), "the subtraction must fail: {case}"),
}
}
for case in file
.get("multiply")
.and_then(Value::as_array)
.expect("multiply")
{
let k: u64 = fixtures::field(case, "k")
.expect("k")
.parse()
.expect("a u64");
let outcome = rational(case.get("a").expect("a")).checked_mul_u64(k);
match case.get("product") {
Some(product) => assert_eq!(outcome.expect("a product"), rational(product)),
None => assert!(outcome.is_err(), "the product must overflow: {case}"),
}
}
for case in file
.get("compare")
.and_then(Value::as_array)
.expect("compare")
{
let left = rational(case.get("a").expect("a"));
let right = rational(case.get("b").expect("b"));
let ordering = match fixtures::field(case, "ordering").expect("ordering") {
"less" => std::cmp::Ordering::Less,
"equal" => std::cmp::Ordering::Equal,
"greater" => std::cmp::Ordering::Greater,
other => panic!("unknown ordering {other:?}"),
};
assert_eq!(left.cmp(&right), ordering, "{case}");
}
}
#[test]
fn zero_has_exactly_one_encoding_and_durations_must_be_positive() {
assert_eq!(RationalNs::ZERO, RationalNs::new(0, 1).expect("0/1"));
assert!(RationalNs::new(0, 2).is_err(), "zero is encoded 0/1");
assert!(RationalNs::new(1, 0).is_err(), "denominators are positive");
assert!(RationalNs::new(2, 4).is_err(), "fractions are reduced");
assert!(RationalNs::ZERO.require_positive("worldTime").is_err());
assert!(
RationalNs::new(1, 3)
.expect("1/3")
.require_positive("tickDuration")
.is_ok()
);
assert!(
RationalNs::ZERO.divide_floor(&RationalNs::ZERO).is_err(),
"dividing by a zero tick is refused, not infinite"
);
}
#[test]
fn reduction_refuses_a_result_that_does_not_fit_u64() {
let big = RationalNs::new(u64::MAX, 1).expect("a whole number");
assert!(big.checked_mul_u64(2).is_err());
assert!(big.checked_add(&big).is_err());
assert_eq!(
RationalNs::reduced(u128::from(u64::MAX) * 2, 2).expect("reduces back into range"),
big
);
}

View file

@ -0,0 +1,187 @@
//! The canonical schema set, `contractDigest` and the freshness of every derived fixture.
use fly_session_types::{canonical, fixtures, schema};
use serde_json::Value;
#[path = "../examples/update_fixtures.rs"]
#[allow(dead_code, reason = "the example's main is not used by the test that reuses its writers")]
mod updater;
/// Every derived fixture is exactly what the updater writes today. If this fails, run
/// `cargo run -p fly-session-types --example update_fixtures` and review the diff.
#[test]
fn derived_fixtures_are_current() {
for (name, expected) in updater::derived() {
let path = fixtures::dir().join(&name);
let found = std::fs::read_to_string(&path).expect("a checked-in fixture");
assert_eq!(
found, expected,
"{name} is stale; regenerate it with the update_fixtures example"
);
}
}
#[test]
fn the_contract_digest_is_the_digest_of_the_checked_in_schema_set() {
let text = fixtures::load_bytes("schema-set.json").expect("schema-set.json");
let recorded = fixtures::load("contract-digest.json").expect("contract-digest.json");
let expected = recorded
.get("contractDigest")
.and_then(Value::as_str)
.expect("contractDigest");
assert_eq!(schema::contract_digest(), expected);
// The file is the canonical schema set plus one trailing newline.
assert_eq!(
canonical::sha256_hex(text.strip_suffix(b"\n").expect("trailing newline")),
expected,
"the digest is over the canonical schema set, byte for byte"
);
}
/// The digest comes from the schema declaration, not from the source text: reparsing the
/// checked-in file with different whitespace and key order gives the same digest.
#[test]
fn the_contract_digest_survives_reformatting() {
let bytes = fixtures::load_bytes("schema-set.json").expect("schema-set.json");
let parsed = canonical::parse_strict(bytes.strip_suffix(b"\n").expect("newline")).expect("parses");
let pretty = serde_json::to_vec_pretty(&parsed).expect("serializable");
let reparsed = canonical::parse_strict(&pretty).expect("parses");
assert_eq!(
canonical::digest_of(&reparsed).expect("digest"),
schema::contract_digest(),
"pretty printing the schema set does not change its digest"
);
let shuffled = canonical::parse_strict(
br#"{"version":1,"contract":"fly-session-types-other"}"#,
)
.expect("parses");
assert_ne!(
canonical::digest_of(&shuffled).expect("digest"),
schema::contract_digest()
);
}
/// ... and it changes when a schema changes: a renamed field, a widened bound, one more enum
/// member or one fewer type all move the digest.
#[test]
fn the_contract_digest_changes_when_a_schema_changes() {
let baseline = schema::contract_digest();
let mutate = |mutation: fn(&mut Value)| {
let mut set = schema::schema_set();
mutation(&mut set);
canonical::digest_of(&set).expect("digest")
};
let renamed_field = mutate(|set| {
set["types"][0]["fields"][0]["name"] = Value::String("sessionIdentifier".to_owned());
});
let widened_bound = mutate(|set| {
for limit in set["limits"].as_array_mut().expect("limits") {
if limit["name"] == Value::String("maxAgents".to_owned()) {
limit["value"] = Value::from(8u64);
}
}
});
let extra_enum_member = mutate(|set| {
set["enums"][0]["members"]
.as_array_mut()
.expect("members")
.push(Value::String("s16le-interleaved".to_owned()));
});
let dropped_type = mutate(|set| {
set["types"].as_array_mut().expect("types").pop();
});
let relaxed_constraint = mutate(|set| {
set["types"][0]["fields"][0]["constraint"] = Value::String("anything".to_owned());
});
for (what, digest) in [
("a renamed field", renamed_field),
("a widened bound", widened_bound),
("an extra enum member", extra_enum_member),
("a dropped type", dropped_type),
("a relaxed constraint", relaxed_constraint),
] {
assert_ne!(digest, baseline, "{what} must change contractDigest");
}
}
#[test]
fn the_schema_set_names_every_type_the_crate_reads() {
let set = schema::schema_set();
let names: Vec<&str> = set["types"]
.as_array()
.expect("types")
.iter()
.map(|t| t["name"].as_str().expect("name"))
.collect();
for expected in [
"Scope",
"RationalNs",
"TypedValue",
"SessionRpcRequest",
"SessionRpcFailure",
"PrepareParams",
"StepResult",
"ViewRef",
"AudioRef",
"CaptureResult",
"SessionDescriptor",
"CommittedSnapshot",
"TraceBehaviour",
"TraceOperational",
] {
assert!(names.contains(&expected), "the schema set must name {expected}");
}
let mut sorted = names.clone();
sorted.sort_unstable();
assert_eq!(names, sorted, "the rendered set is sorted by type name");
let mut unique = sorted.clone();
unique.dedup();
assert_eq!(unique.len(), names.len(), "no type is declared twice");
}
/// Every bound the schema set publishes is the constant the code enforces, and every bound
/// this crate chose rather than read from a document says so.
#[test]
fn published_limits_match_the_constants_and_name_their_source() {
let set = schema::schema_set();
let limits = set["limits"].as_array().expect("limits");
let find = |name: &str| -> u64 {
limits
.iter()
.find(|l| l["name"] == Value::String(name.to_owned()))
.and_then(|l| l["value"].as_u64())
.unwrap_or_else(|| panic!("the schema set must publish {name}"))
};
assert_eq!(find("maxAgents"), fly_session_types::workers::MAX_AGENTS as u64);
assert_eq!(find("maxPorts"), fly_session_types::workers::MAX_PORTS as u64);
assert_eq!(
find("maxRateRoles"),
fly_session_types::workers::MAX_RATE_ROLES as u64
);
assert_eq!(find("maxViews"), fly_session_types::media::MAX_VIEWS as u64);
assert_eq!(
find("maxTypedValueBytes"),
fly_session_types::scalar::MAX_TYPED_VALUE_BYTES as u64
);
assert_eq!(
find("maxEnvelopeBytes"),
canonical::MAX_ENVELOPE_BYTES as u64
);
let crate_chosen: Vec<&str> = limits
.iter()
.filter(|l| l["source"] == Value::String("crate".to_owned()))
.map(|l| l["name"].as_str().expect("name"))
.collect();
assert_eq!(
crate_chosen,
[
"maxAssets",
"maxAudioStreams",
"maxCapabilities",
"maxSnapshotEvents",
"maxSupportedMajors",
"maxSupportedStimuli",
],
"a bound with no stated source must be declared as this crate's choice"
);
}

View file

@ -0,0 +1,120 @@
//! `seed-derivation-v1` against its test vectors.
use fly_session_types::{fixtures, seed};
use serde_json::Value;
#[test]
fn every_vector_derives_its_recorded_seed() {
let file = fixtures::load("seed-vectors.json").expect("seed-vectors.json");
assert_eq!(
file.get("algorithm").and_then(Value::as_str),
Some(seed::ALGORITHM)
);
let vectors = file.get("vectors").and_then(Value::as_array).expect("vectors");
for case in vectors {
let master: u64 = fixtures::field(case, "masterSeed")
.expect("masterSeed")
.parse()
.expect("a U64");
let agent = fixtures::field(case, "agentId").expect("agentId");
assert_eq!(
String::from_utf8(seed::material(master, agent).expect("material")).expect("utf-8"),
fixtures::field(case, "material").expect("material"),
"the hashed material is part of the specification"
);
assert_eq!(
seed::material_digest(master, agent).expect("digest"),
fixtures::field(case, "materialDigest").expect("materialDigest")
);
assert_eq!(
i64::from(seed::agent_seed(master, agent).expect("seed")),
case.get("seed").and_then(Value::as_i64).expect("seed"),
"seed for {agent} under master {master}"
);
}
assert!(vectors.len() >= 20, "keep the vector table broad");
}
#[test]
fn one_composition_gets_independent_seeds() {
let file = fixtures::load("seed-vectors.json").expect("seed-vectors.json");
let composition = file.get("composition").expect("composition");
let master: u64 = fixtures::field(composition, "masterSeed")
.expect("masterSeed")
.parse()
.expect("a U64");
let ids: Vec<String> = composition
.get("agentIds")
.and_then(Value::as_array)
.expect("agentIds")
.iter()
.map(|v| v.as_str().expect("an id").to_owned())
.collect();
let seeds = seed::composition_seeds(master, &ids).expect("seeds");
let recorded: Vec<i64> = composition
.get("seeds")
.and_then(Value::as_array)
.expect("seeds")
.iter()
.map(|v| v.as_i64().expect("a seed"))
.collect();
assert_eq!(
seeds.iter().map(|s| i64::from(*s)).collect::<Vec<_>>(),
recorded
);
let mut unique = seeds.clone();
unique.sort_unstable();
unique.dedup();
assert_eq!(unique.len(), seeds.len(), "per-agent seeds are independent");
assert!(
seeds.iter().all(|s| *s != 0),
"a zero seed would stall an xorshift generator"
);
}
#[test]
fn a_different_master_seed_or_agent_id_derives_a_different_seed() {
assert_ne!(
seed::agent_seed(0, "fly-a").expect("seed"),
seed::agent_seed(1, "fly-a").expect("seed")
);
assert_ne!(
seed::agent_seed(0, "fly-a").expect("seed"),
seed::agent_seed(0, "fly-b").expect("seed")
);
assert_eq!(
seed::agent_seed(7, "fly-a").expect("seed"),
seed::agent_seed(7, "fly-a").expect("seed"),
"the derivation is a function of its recorded inputs"
);
}
#[test]
fn invalid_inputs_are_refused_rather_than_normalized() {
let file = fixtures::load("seed-vectors.json").expect("seed-vectors.json");
for case in file.get("invalid").and_then(Value::as_array).expect("invalid") {
let master: u64 = fixtures::field(case, "masterSeed")
.expect("masterSeed")
.parse()
.expect("a U64");
if let Ok(agent) = fixtures::field(case, "agentId") {
assert!(
seed::agent_seed(master, agent).is_err(),
"{agent:?} must be refused: {}",
fixtures::field(case, "reason").unwrap_or("")
);
} else {
let ids: Vec<String> = case
.get("agentIds")
.and_then(Value::as_array)
.expect("agentIds")
.iter()
.map(|v| v.as_str().expect("an id").to_owned())
.collect();
assert!(
seed::composition_seeds(master, &ids).is_err(),
"a repeated agent id must be refused"
);
}
}
}

View file

@ -0,0 +1,113 @@
//! The step-v1 section 8 trace comparator: behaviour only.
use fly_session_types::fixtures;
use fly_session_types::scalar::DomainType;
use fly_session_types::trace::TransitionTrace;
use serde_json::Value;
fn trace(value: &Value) -> TransitionTrace {
TransitionTrace::from_json(value).expect("a valid trace")
}
#[test]
fn every_variant_compares_the_way_the_fixture_says() {
let file = fixtures::load("traces.json").expect("traces.json");
let baseline = trace(file.get("baseline").expect("baseline"));
for case in file
.get("variants")
.and_then(Value::as_array)
.expect("variants")
{
let name = fixtures::field(case, "name").expect("name");
let variant = trace(case.get("trace").expect("trace"));
let expected = case
.get("behaviourEquals")
.and_then(Value::as_bool)
.expect("behaviourEquals");
let equal = baseline.behaviour_equals(&variant);
let diff = baseline.behaviour_diff(&variant);
assert_eq!(
equal, expected,
"{name}: behaviour equality. differences: {diff:?}"
);
assert_eq!(
diff.is_empty(),
expected,
"{name}: the diff must be empty exactly when the behaviour matches"
);
if let Ok(needle) = fixtures::field(case, "diffContains") {
assert!(
diff.iter().any(|line| line.contains(needle)),
"{name}: the diff should name {needle:?}, got {diff:?}"
);
}
if expected {
assert_eq!(
baseline.behaviour.digest().expect("digest"),
variant.behaviour.digest().expect("digest"),
"{name}: equal behaviour has one digest"
);
}
}
}
#[test]
fn a_whole_run_compares_transition_by_transition() {
let file = fixtures::load("traces.json").expect("traces.json");
let baseline = trace(file.get("baseline").expect("baseline"));
let variants = file
.get("variants")
.and_then(Value::as_array)
.expect("variants");
let reversed = trace(variants[0].get("trace").expect("trace"));
let changed = trace(
variants
.iter()
.find(|case| fixtures::field(case, "name").unwrap_or("") == "one extra neural tick")
.expect("the extra tick variant")
.get("trace")
.expect("trace"),
);
assert!(TransitionTrace::runs_equal(
&[baseline.clone(), baseline.clone()],
&[reversed, baseline.clone()]
));
assert!(!TransitionTrace::runs_equal(
std::slice::from_ref(&baseline),
&[changed]
));
let longer = [baseline.clone(), baseline.clone()];
assert!(
!TransitionTrace::runs_equal(std::slice::from_ref(&baseline), &longer),
"a run with more transitions is not the same run"
);
}
/// The operational half is recorded, and never part of the comparison.
#[test]
fn operational_metadata_is_recorded_and_excluded() {
let file = fixtures::load("traces.json").expect("traces.json");
let baseline = trace(file.get("baseline").expect("baseline"));
assert_eq!(baseline.operational.bus_call_ids.len(), 3);
assert_eq!(baseline.operational.prepare_request_ids.len(), 2);
assert_eq!(baseline.operational.delivery_ids.len(), 2);
assert!(baseline.operational.wall_time_ns > 0);
let retried = trace(
file.get("variants")
.and_then(Value::as_array)
.expect("variants")
.iter()
.find(|case| {
fixtures::field(case, "name").unwrap_or("")
== "a safe retry with fresh bus callIds, delivery ids and wall time"
})
.expect("the retry variant")
.get("trace")
.expect("trace"),
);
assert_ne!(
baseline.operational, retried.operational,
"the retry really did change the operational half"
);
assert!(baseline.behaviour_equals(&retried));
}