makepad/libs/asset/data/tests/canonical.rs
Admin 2d23dba736 libs/asset: the store runs on our own SQLite, and the importers learn the whole map contract
The asset store now uses libs/sqlite_query as its ONLY engine — not a feature
flag, not a fallback. That closes the Windows gap (the embedded store starts
there now, and a SHARED->EXCLUSIVE upgrade is handled rather than assumed
free) and takes the C dependency out of the build everywhere else.

Around it:

  - store: a garbage collector, catalogued content that is referenced in place
    instead of copied, the `vjeffect` kind, and host/chat routes that keep up
    with the chat wire below.
  - importer: the unified map contract reaches quake2, quake3, doom and duke —
    world placement, nav, welding, prelit maps and glTF node handling shared
    rather than reimplemented per game. Music import, billboards and stateful
    props move to the data crate so readers stop linking the importer.
  - ai: the serving side of multi-lane chat — per-lane conversations, honest
    progress and acceptance reporting, penalties and a watchdog, context as a
    per-box number that compacts instead of erupting, a realtime session mode,
    and inpaint/flux2 backends. `chat_bench` measures the rate the way the
    client meter computes it.
  - client / chat / chat_ui: a publication can NAME a file instead of carrying
    it; the wire says whether a turn is warm and whether it is thinking, so a
    client stops guessing; transcript and feed widgets render history the way
    the model wrote it. `SessionConfig::catalog_runtime` lets a host size the
    catalog runtime's lanes itself — a browsing UI puts every listing, every
    per-tile resolve and every thumbnail blob through that one runtime and
    wants a wider fast lane than the shared default, while media lanes keep
    it (a few big transfers, not a thousand small ones).
  - widgets: the shared asset widgets — one video view (knobbed seek,
    transport, bracket trim, rail playback) used everywhere, plus thumb,
    preview, scene view, walk-world and the lyric reader.
2026-08-23 01:34:34 +02: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()
);
}