makepad/libs/asset/data/tests/canonical.rs
Arena Agent d8d5ea8b6a Repair integration manifests and port nigig exports onto upstream widget families
Restore missing required asset/flow packages from the pinned nigig revision; retain upstream script isolation, X11 hover, and widget-core fixes. Validated makepad-widgets data feature with Rust 1.97.1.
2026-10-26 11:23:26 +00:00

479 lines
16 KiB
Rust

//! Canonical encoding/hashing fixtures: two independent processes must
//! encode the same manifests to the same hash.
//!
//! The golden digests below are the frozen contract for schema version 1. If
//! one changes, the encoding changed for every peer: that is a schema bump,
//! not a test update.
mod common;
use common::*;
use makepad_asset_data::*;
// Frozen golden digests for CONTENT_SCHEMA_VERSION 3.
const GOLDEN_WEAPON_REVISION: &str =
"arev_db582d7e062bcafc01c4937274a83e996602dd102735b822db91787bd4d42d15";
const GOLDEN_LOCK_BLOB: &str =
"sha256:2fd64e6bf54c34a297be22e34427e32a53c26cafcd3457b62c803402551a4af2";
const GOLDEN_GAME_REVISION: &str =
"grev_02a07e4e1d441805d1307d6870e59c277326ca53878111f996e7f12a89f46f0e";
const GOLDEN_BASELINE_SET: &str =
"cset_f44e42b23470481a5bb6ddd0b902b9e5241a59e22c735264b4b7a06c14d204e4";
const GOLDEN_SCENE_PLAN: &str =
"splan_090f628b81ee71f931a09015b859adf3aac82b579656fcd26cf74ecdd53b0934";
const GOLDEN_MIGRATION_PLAN: &str =
"mplan_0d567e82213e671e308509f85d8b991b8984ef9dfadc83fc68f3c464e4372f03";
#[test]
fn asset_manifest_roundtrip_and_golden_digest() {
let manifest = weapon_manifest();
let bytes = manifest.to_canonical_bytes().unwrap();
let back = AssetManifest::from_canonical_bytes(&bytes).unwrap();
assert_eq!(back, manifest);
// Encode-decode-encode is byte-identical: the encoding is a bijection on
// accepted documents.
assert_eq!(back.to_canonical_bytes().unwrap(), bytes);
assert_eq!(manifest.revision().unwrap().to_string(), GOLDEN_WEAPON_REVISION);
}
#[test]
fn producer_field_order_cannot_change_the_hash() {
// A producer that filled its vectors in reverse (map iteration, discovery
// order) canonicalizes to the identical bytes...
let mut shuffled = weapon_manifest();
shuffled.files.reverse();
shuffled.dependencies.reverse();
shuffled.anchors.reverse();
shuffled.spawn_recipe.as_mut().unwrap().params.reverse();
// ...but is refused, not silently reordered, if it skips canonicalize.
assert!(matches!(
shuffled.to_canonical_bytes(),
Err(AssetDataError::NotSorted { .. })
));
shuffled.canonicalize();
assert_eq!(
shuffled.to_canonical_bytes().unwrap(),
weapon_manifest().to_canonical_bytes().unwrap()
);
}
#[test]
fn lock_baseline_set_and_game_revision_goldens() {
let lock = lock();
assert_eq!(lock.blob_id().unwrap().to_string(), GOLDEN_LOCK_BLOB);
let game = game_revision_manifest();
let game_rev = game.revision().unwrap();
assert_eq!(game_rev.to_string(), GOLDEN_GAME_REVISION);
let baseline = ContentSetManifest::baseline(game_rev, &lock).unwrap();
assert_eq!(baseline.id().unwrap().to_string(), GOLDEN_BASELINE_SET);
// The baseline slot table is the lock closure in canonical order.
assert_eq!(baseline.slots, lock.closure);
assert_eq!(baseline.get(AssetSlot(0)), Some(&aref(1, 0x11)));
assert_eq!(baseline.slot_of(&aref(4, 0x41)), Some(AssetSlot(3)));
// Alias resolution against the lock is exact.
let alias: AssetAlias = "rik2/weapons/fancy-rocket-launcher".parse().unwrap();
assert_eq!(lock.resolve(&alias), Some(aref(1, 0x11)));
let missing: AssetAlias = "rik2/weapons/unknown".parse().unwrap();
assert_eq!(lock.resolve(&missing), None);
let bytes = lock.to_canonical_bytes().unwrap();
assert_eq!(ContentLock::from_canonical_bytes(&bytes).unwrap(), lock);
let bytes = game.to_canonical_bytes().unwrap();
assert_eq!(
GameRevisionManifest::from_canonical_bytes(&bytes).unwrap(),
game
);
}
#[test]
fn content_set_extension_is_append_only_and_verified() {
let lock = lock();
let game_rev = game_revision_manifest().revision().unwrap();
let baseline = ContentSetManifest::baseline(game_rev, &lock).unwrap();
// Extension appends in canonical order regardless of the caller's order.
let extended = baseline.extended(&[aref(6, 0x61), aref(5, 0x51)]).unwrap();
assert_eq!(extended.parent, Some(baseline.id().unwrap()));
assert_eq!(extended.slots.len(), baseline.slots.len() + 2);
assert_eq!(
extended.get(AssetSlot(baseline.slots.len() as u32)),
Some(&aref(5, 0x51))
);
// Existing slots keep their exact assignment.
for (i, slot) in baseline.slots.iter().enumerate() {
assert_eq!(extended.get(AssetSlot(i as u32)), Some(slot));
}
extended.extends(&baseline).unwrap();
// Chained extension still verifies against its own parent.
let extended2 = extended.extended(&[aref(8, 0x81)]).unwrap();
extended2.extends(&extended).unwrap();
assert!(extended2.extends(&baseline).is_err());
let bytes = extended.to_canonical_bytes().unwrap();
assert_eq!(
ContentSetManifest::from_canonical_bytes(&bytes).unwrap(),
extended
);
}
#[test]
fn scene_and_migration_plan_goldens() {
let lock = lock();
let game_rev = game_revision_manifest().revision().unwrap();
let set_id = ContentSetManifest::baseline(game_rev, &lock)
.unwrap()
.id()
.unwrap();
let plan = scene_plan(game_rev, set_id);
assert_eq!(plan.digest().unwrap().to_string(), GOLDEN_SCENE_PLAN);
let bytes = plan.to_canonical_bytes().unwrap();
assert_eq!(ScenePlan::from_canonical_bytes(&bytes).unwrap(), plan);
let key: SceneObjectKey = "arena/main_gate".parse().unwrap();
assert!(plan.object(&key).is_some());
assert_eq!(plan.asset_refs().count(), 1);
let to_rev = GameRevisionId::hash_of(b"next");
let mig = migration_plan(game_rev, to_rev);
assert_eq!(mig.digest().unwrap().to_string(), GOLDEN_MIGRATION_PLAN);
let bytes = mig.to_canonical_bytes().unwrap();
assert_eq!(SceneMigrationPlan::from_canonical_bytes(&bytes).unwrap(), mig);
assert_eq!(mig.verified_minimum(), ActivationMode::Migrate);
// Reason discovery order cannot change the plan digest: shuffled input
// canonicalizes to the identical bytes.
let mut shuffled = migration_plan(game_rev, to_rev);
shuffled.reasons.reverse();
shuffled.canonicalize();
assert_eq!(shuffled.to_canonical_bytes().unwrap(), bytes);
}
#[test]
fn activation_dtos_roundtrip() {
let game_rev = GameRevisionId::hash_of(b"g");
let set = ContentSetId::hash_of(b"s");
let ticket = JoinTicket {
transaction_id: txn(1),
realm_epoch: RealmEpoch(3),
game_revision: game_rev,
content_set: set,
};
let descriptor = RealmDescriptor {
ticket,
variant_policy_version: RESOLUTION_POLICY_V1,
origins: vec!["http://192.0.2.7:8770".into()],
read_capability: vec![1, 2, 3],
};
let bytes = descriptor.to_canonical_bytes().unwrap();
assert_eq!(
RealmDescriptor::from_canonical_bytes(&bytes).unwrap(),
descriptor
);
let ready = JoinContentReady {
ticket,
resolution: rres(5),
};
let bytes = ready.to_canonical_bytes().unwrap();
assert_eq!(JoinContentReady::from_canonical_bytes(&bytes).unwrap(), ready);
let scene_ready = SceneContentReady {
transaction_id: txn(2),
realm_epoch: RealmEpoch(3),
next_game_revision: GameRevisionId::hash_of(b"g2"),
next_content_set: set,
resolution: rres(5),
};
let bytes = scene_ready.to_canonical_bytes().unwrap();
assert_eq!(
SceneContentReady::from_canonical_bytes(&bytes).unwrap(),
scene_ready
);
let prepare = PrepareSceneChange {
transaction_id: txn(2),
realm_epoch: RealmEpoch(3),
parent_game_revision: game_rev,
next_game_revision: GameRevisionId::hash_of(b"g2"),
parent_scene_sequence: SceneSequence(7),
next_content_set: set,
scene_plan_digest: ScenePlanDigest::hash_of(b"p"),
migration_plan_digest: MigrationPlanDigest::hash_of(b"m"),
activation_mode: ActivationMode::HotPatch,
proposed_activation_tick: Tick(9000),
readiness: readiness(),
};
let bytes = prepare.to_canonical_bytes().unwrap();
assert_eq!(
PrepareSceneChange::from_canonical_bytes(&bytes).unwrap(),
prepare
);
let commit = CommitSceneChange {
transaction_id: txn(2),
realm_epoch: RealmEpoch(3),
next_game_revision: GameRevisionId::hash_of(b"g2"),
next_scene_sequence: SceneSequence(8),
next_content_set: set,
activation_tick: Tick(9060),
};
let bytes = commit.to_canonical_bytes().unwrap();
assert_eq!(
CommitSceneChange::from_canonical_bytes(&bytes).unwrap(),
commit
);
let applied = SceneApplied {
transaction_id: txn(2),
realm_epoch: RealmEpoch(3),
next_scene_sequence: SceneSequence(8),
};
let bytes = applied.to_canonical_bytes().unwrap();
assert_eq!(SceneApplied::from_canonical_bytes(&bytes).unwrap(), applied);
let prep_content = PrepareContentChange {
transaction_id: txn(4),
realm_epoch: RealmEpoch(3),
next_set: ContentSetId::hash_of(b"s2"),
required_delta: vec![aref(5, 0x51)],
proposed_activation_tick: Tick(10_000),
readiness: readiness(),
};
let bytes = prep_content.to_canonical_bytes().unwrap();
assert_eq!(
PrepareContentChange::from_canonical_bytes(&bytes).unwrap(),
prep_content
);
let ready = ContentChangeReady {
transaction_id: txn(4),
realm_epoch: RealmEpoch(3),
next_set: ContentSetId::hash_of(b"s2"),
resolution: rres(5),
};
let bytes = ready.to_canonical_bytes().unwrap();
assert_eq!(
ContentChangeReady::from_canonical_bytes(&bytes).unwrap(),
ready
);
let commit = CommitContentChange {
transaction_id: txn(4),
realm_epoch: RealmEpoch(3),
next_set: ContentSetId::hash_of(b"s2"),
dynamic_spawn_slots: vec![AssetSlot(4), AssetSlot(4)],
activation_tick: Tick(10_060),
};
let bytes = commit.to_canonical_bytes().unwrap();
assert_eq!(
CommitContentChange::from_canonical_bytes(&bytes).unwrap(),
commit
);
let prep_realm = PrepareRealm {
transaction_id: txn(5),
next_epoch: RealmEpoch(4),
game_revision: game_rev,
content_set: set,
readiness: readiness(),
};
let bytes = prep_realm.to_canonical_bytes().unwrap();
assert_eq!(PrepareRealm::from_canonical_bytes(&bytes).unwrap(), prep_realm);
let commit_realm = CommitRealm {
transaction_id: txn(5),
next_epoch: RealmEpoch(4),
game_revision: game_rev,
content_set: set,
};
let bytes = commit_realm.to_canonical_bytes().unwrap();
assert_eq!(CommitRealm::from_canonical_bytes(&bytes).unwrap(), commit_realm);
let tuple = RoomContentTuple {
realm_epoch: RealmEpoch(3),
game_revision: game_rev,
scene_sequence: SceneSequence(8),
content_set: set,
};
let bytes = tuple.to_canonical_bytes().unwrap();
assert_eq!(RoomContentTuple::from_canonical_bytes(&bytes).unwrap(), tuple);
}
#[test]
fn wrong_or_stale_resolution_cannot_ready_a_join() {
let expected_ticket = ticket(1);
let expected = rres(5);
let ready = JoinContentReady {
ticket: expected_ticket,
resolution: expected,
};
ready.verify(&expected_ticket, &expected).unwrap();
// Correct ticket, wrong aggregate digest: refused. A peer that resolved
// against different variants can never be embodied.
let wrong = JoinContentReady {
ticket: expected_ticket,
resolution: rres(6),
};
assert!(matches!(
wrong.verify(&expected_ticket, &expected),
Err(AssetDataError::Mismatch { what: "join ready resolution" })
));
// Stale ticket from an abandoned preparation: refused even with the
// aggregate the host currently expects.
let stale = JoinContentReady {
ticket: ticket(9),
resolution: expected,
};
assert!(matches!(
stale.verify(&expected_ticket, &expected),
Err(AssetDataError::Mismatch { what: "join ready ticket" })
));
// A cache fill or preference advance mid-join changes the expected
// aggregate; the previously acknowledged digest no longer verifies.
assert!(ready.verify(&expected_ticket, &rres(7)).is_err());
}
#[test]
fn multi_asset_realm_resolution_matches_the_lock_pin_table() {
let lock = lock();
let set = ContentSetId::hash_of(b"s");
// The aggregate covers exactly the lock's pinned (base, set) pairs, in
// the same canonical order, with this client's per-asset map digests.
let mut resolution = RealmResolution {
content_set: set,
profile: ClientProfileDigest::hash_of(b"p"),
entries: vec![
RealmResolutionEntry {
base: aref(4, 0x41),
variant_set: vset_id(0xd4),
resolved_map: rmap(0x24),
},
RealmResolutionEntry {
base: aref(1, 0x11),
variant_set: vset_id(0xd1),
resolved_map: rmap(0x21),
},
],
};
resolution.canonicalize();
resolution.verify_against(&lock).unwrap();
// Roundtrip and a stable digest for readiness.
let bytes = resolution.to_canonical_bytes().unwrap();
assert_eq!(
RealmResolution::from_canonical_bytes(&bytes).unwrap(),
resolution
);
let digest = resolution.digest().unwrap();
let ready = JoinContentReady {
ticket: ticket(1),
resolution: digest,
};
ready.verify(&ticket(1), &digest).unwrap();
// Skipping a pinned asset refuses: coverage is exact, not best-effort.
let mut partial = resolution.clone();
partial.entries.pop();
assert!(matches!(
partial.verify_against(&lock),
Err(AssetDataError::Mismatch { what: "realm resolution coverage" })
));
// Resolving against a set the release never pinned refuses.
let mut wrong_set = resolution.clone();
wrong_set.entries[0].variant_set = vset_id(0xdd);
assert!(matches!(
wrong_set.verify_against(&lock),
Err(AssetDataError::Mismatch { what: "realm resolution pinned set" })
));
// Two resolutions for one asset refuse outright.
let mut doubled = resolution.clone();
doubled.entries.push(RealmResolutionEntry {
base: aref(4, 0x42),
variant_set: vset_id(0xd4),
resolved_map: rmap(0x25),
});
doubled.canonicalize();
assert!(matches!(
doubled.validate(),
Err(AssetDataError::Duplicate { .. })
));
}
#[test]
fn empty_realm_resolution_readies_a_no_variant_realm() {
// A lock that pins no variant sets still yields a canonical, digestable
// aggregate, so plain realms can ready.
let mut no_variants = lock();
no_variants.variant_sets.clear();
no_variants.validate().unwrap();
let resolution = RealmResolution {
content_set: ContentSetId::hash_of(b"s"),
profile: ClientProfileDigest::hash_of(b"p"),
entries: vec![],
};
resolution.verify_against(&no_variants).unwrap();
let bytes = resolution.to_canonical_bytes().unwrap();
assert_eq!(
RealmResolution::from_canonical_bytes(&bytes).unwrap(),
resolution
);
let digest = resolution.digest().unwrap();
let ready = JoinContentReady {
ticket: ticket(1),
resolution: digest,
};
ready.verify(&ticket(1), &digest).unwrap();
// The digest still binds content set and profile: a different profile
// or set yields a different empty-aggregate digest.
let other_profile = RealmResolution {
content_set: ContentSetId::hash_of(b"s"),
profile: ClientProfileDigest::hash_of(b"q"),
entries: vec![],
};
assert_ne!(other_profile.digest().unwrap(), digest);
// But an empty aggregate cannot ready a realm that DOES pin sets.
assert!(resolution.verify_against(&lock()).is_err());
}
#[test]
fn document_kinds_do_not_cross_decode() {
let manifest_bytes = weapon_manifest().to_canonical_bytes().unwrap();
assert!(matches!(
ContentLock::from_canonical_bytes(&manifest_bytes),
Err(AssetDataError::BadDocKind { .. })
));
let lock_bytes = lock().to_canonical_bytes().unwrap();
assert!(matches!(
AssetManifest::from_canonical_bytes(&lock_bytes),
Err(AssetDataError::BadDocKind { .. })
));
}
#[test]
fn negative_zero_is_normalized_to_one_encoding() {
let mut a = weapon_manifest();
a.anchors[0].transform.pos = Vec3::new(0.0, -0.1, 0.2);
let mut b = a.clone();
b.anchors[0].transform.pos.x = -0.0;
assert_eq!(
a.to_canonical_bytes().unwrap(),
b.to_canonical_bytes().unwrap()
);
}