From f0931b47f7c8e7d677ebfcc7d58144990ad58b35 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Thu, 30 Oct 2025 02:33:23 -0700 Subject: [PATCH 1/3] tests(core): add golden motion fixtures (JSON) + harness --- .../tests/fixtures/motion-fixtures.json | 8 +++ .../rmg-core/tests/motion_golden_fixtures.rs | 72 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 crates/rmg-core/tests/fixtures/motion-fixtures.json create mode 100644 crates/rmg-core/tests/motion_golden_fixtures.rs diff --git a/crates/rmg-core/tests/fixtures/motion-fixtures.json b/crates/rmg-core/tests/fixtures/motion-fixtures.json new file mode 100644 index 00000000..7de05d1c --- /dev/null +++ b/crates/rmg-core/tests/fixtures/motion-fixtures.json @@ -0,0 +1,8 @@ +{ + "cases": [ + { "label": "motion-fixture-1", "pos": [1.0, 2.0, 3.0], "vel": [0.5, -1.0, 0.25], "expected_pos": [1.5, 1.0, 3.25] }, + { "label": "motion-fixture-2", "pos": [10.0, -2.0, 3.5], "vel": [0.125, 2.0, -1.5], "expected_pos": [10.125, 0.0, 2.0] }, + { "label": "motion-fixture-3", "pos": [0.0, 0.0, 0.0], "vel": [0.0, 0.0, 0.0], "expected_pos": [0.0, 0.0, 0.0] } + ] +} + diff --git a/crates/rmg-core/tests/motion_golden_fixtures.rs b/crates/rmg-core/tests/motion_golden_fixtures.rs new file mode 100644 index 00000000..26a512cb --- /dev/null +++ b/crates/rmg-core/tests/motion_golden_fixtures.rs @@ -0,0 +1,72 @@ +#![allow(missing_docs)] +use once_cell::sync::Lazy; +use serde::Deserialize; + +use rmg_core::{ + decode_motion_payload, encode_motion_payload, make_node_id, make_type_id, ApplyResult, Engine, + GraphStore, NodeRecord, MOTION_RULE_NAME, +}; + +static RAW: &str = include_str!("fixtures/motion-fixtures.json"); + +#[derive(Debug, Deserialize)] +struct MotionCase { + label: String, + pos: [f32; 3], + vel: [f32; 3], + expected_pos: [f32; 3], +} + +#[derive(Debug, Deserialize)] +struct MotionFixtures { + cases: Vec, +} + +static FIXTURES: Lazy = + Lazy::new(|| serde_json::from_str(RAW).expect("parse motion fixtures")); + +#[test] +fn motion_golden_fixtures_apply_as_expected() { + let entity_ty = make_type_id("entity"); + for case in &FIXTURES.cases { + let ent = make_node_id(&case.label); + // Create a fresh engine and insert entity with payload + let mut store = GraphStore::default(); + let payload = encode_motion_payload(case.pos, case.vel); + store.insert_node( + ent, + NodeRecord { + ty: entity_ty, + payload: Some(payload), + }, + ); + let mut engine = Engine::new(store, ent); + engine + .register_rule(rmg_core::motion_rule()) + .expect("register motion rule"); + + let tx = engine.begin(); + let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply"); + assert!(matches!(res, ApplyResult::Applied)); + engine.commit(tx).expect("commit"); + + // Verify payload bytes decode to expected values + let node = engine.node(&ent).expect("node exists"); + let (pos, vel) = + decode_motion_payload(node.payload.as_ref().expect("payload")).expect("decode"); + for i in 0..3 { + assert_eq!( + vel[i].to_bits(), + case.vel[i].to_bits(), + "vel component {}", + i + ); + assert_eq!( + pos[i].to_bits(), + case.expected_pos[i].to_bits(), + "pos component {}", + i + ); + } + } +} From 51af5f3495afd51a1fa0de37603b53a10eb26d42 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Thu, 30 Oct 2025 03:45:58 -0700 Subject: [PATCH 2/3] docs: record PR-01 (golden motion fixtures) in execution plan + decision log --- docs/decision-log.md | 1 + docs/execution-plan.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/decision-log.md b/docs/decision-log.md index c874d9dd..04110f56 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -20,6 +20,7 @@ The following entries use a heading + bullets format for richer context. | 2025-10-30 | rmg-core determinism hardening | Added reachability-only snapshot hashing; closed tx lifecycle; duplicate rule detection; deterministic scheduler drain order; expanded motion payload docs; tests for duplicate rule name/id and no‑op commit. | Locks determinism contract and surfaces API invariants; prepares PR #7 for a safe merge train. | Clippy clean for rmg-core; workspace push withheld pending further feedback. | +| 2025-10-30 | Tests | Add golden motion fixtures (JSON) + minimal harness validating motion rule bytes/values | Establishes deterministic test baseline for motion; supports future benches and tooling | No runtime impact; PR-01 linked to umbrella and milestone | | 2025-10-28 | PR #7 merged | Reachability-only snapshot hashing; ports demo registers rule; guarded ports footprint; scheduler `finalize_tx()` clears `pending`; `PortKey` u30 mask; hooks+CI hardened (toolchain pin, rustdoc fixes). | Determinism + memory hygiene; remove test footguns; pass CI with stable toolchain while keeping rmg-core MSRV=1.68. | Queued follow-ups: #13 (Mat4 canonical zero + MulAssign), #14 (geom train), #15 (devcontainer). | | 2025-10-27 | MWMR reserve gate | Engine calls `scheduler.finalize_tx()` at commit; compact rule id used on execute path; per‑tx telemetry summary behind feature. | Enforce independence and clear active frontier deterministically; keep ordering stable with `(scope_hash, family_id)`. | Toolchain pinned to Rust 1.68; add design note for telemetry graph snapshot replay. | diff --git a/docs/execution-plan.md b/docs/execution-plan.md index 712630d9..fd42734b 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -33,6 +33,12 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s ## Today’s Intent +> 2025-10-30 — PR-01: Golden motion fixtures (tests-only) + +- Add JSON golden fixtures and a minimal harness for the motion rule under `crates/rmg-core/tests/`. +- Scope: tests-only; no runtime changes. +- Links: PR-01 and tracking issue are associated for visibility. + > 2025-10-29 — Geom fat AABB midpoint sampling (merge-train) - Update `rmg-geom::temporal::Timespan::fat_aabb` to union AABBs at start, mid (t=0.5), and end to conservatively bound rotations about off‑centre pivots. From b1eb5658349ea20eda3710aa2c85ee7ed1124cf6 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Thu, 30 Oct 2025 04:42:16 -0700 Subject: [PATCH 3/3] tests(core): fix clippy expect_fun_call in golden fixtures test --- .../tests/fixtures/motion-fixtures.json | 171 +++++++++++++++++- .../rmg-core/tests/motion_golden_fixtures.rs | 77 ++++++-- 2 files changed, 224 insertions(+), 24 deletions(-) diff --git a/crates/rmg-core/tests/fixtures/motion-fixtures.json b/crates/rmg-core/tests/fixtures/motion-fixtures.json index 7de05d1c..72564022 100644 --- a/crates/rmg-core/tests/fixtures/motion-fixtures.json +++ b/crates/rmg-core/tests/fixtures/motion-fixtures.json @@ -1,8 +1,167 @@ { "cases": [ - { "label": "motion-fixture-1", "pos": [1.0, 2.0, 3.0], "vel": [0.5, -1.0, 0.25], "expected_pos": [1.5, 1.0, 3.25] }, - { "label": "motion-fixture-2", "pos": [10.0, -2.0, 3.5], "vel": [0.125, 2.0, -1.5], "expected_pos": [10.125, 0.0, 2.0] }, - { "label": "motion-fixture-3", "pos": [0.0, 0.0, 0.0], "vel": [0.0, 0.0, 0.0], "expected_pos": [0.0, 0.0, 0.0] } - ] -} - + { + "label": "motion-fixture-1", + "pos": [ + 1.0, + 2.0, + 3.0 + ], + "vel": [ + 0.5, + -1.0, + 0.25 + ], + "expected_pos": [ + 1.5, + 1.0, + 3.25 + ] + }, + { + "label": "motion-fixture-2", + "pos": [ + 10.0, + -2.0, + 3.5 + ], + "vel": [ + 0.125, + 2.0, + -1.5 + ], + "expected_pos": [ + 10.125, + 0.0, + 2.0 + ] + }, + { + "label": "motion-fixture-3", + "pos": [ + 0.0, + 0.0, + 0.0 + ], + "vel": [ + 0.0, + 0.0, + 0.0 + ], + "expected_pos": [ + 0.0, + 0.0, + 0.0 + ] + }, + { + "label": "motion-neg-pos", + "pos": [ + -1.0, + -2.0, + -3.0 + ], + "vel": [ + 0.5, + -1.0, + 0.25 + ], + "expected_pos": [ + -0.5, + -3.0, + -2.75 + ] + }, + { + "label": "motion-large", + "pos": [ + 1000000.0, + -1000000.0, + 1000000.0 + ], + "vel": [ + 1000000.0, + 1000000.0, + -1000000.0 + ], + "expected_pos": [ + 2000000.0, + 0.0, + 0.0 + ] + }, + { + "label": "motion-small", + "pos": [ + 1e-06, + -1e-06, + 1e-06 + ], + "vel": [ + 1e-06, + 1e-06, + -1e-06 + ], + "expected_pos": [ + 2e-06, + 0.0, + 0.0 + ] + }, + { + "label": "motion-large+tiny", + "pos": [ + 1000000.0, + 1000000.0, + 1000000.0 + ], + "vel": [ + 1e-06, + -1e-06, + 1e-06 + ], + "expected_pos": [ + 1000000.0, + 1000000.0, + 1000000.0 + ] + }, + { + "label": "motion-subnormal", + "pos": [ + 1.4012985e-45, + 0.0, + -1.4012985e-45 + ], + "vel": [ + 1.4012985e-45, + 1.4012985e-45, + 1.4012985e-45 + ], + "expected_pos": [ + 2.802597e-45, + 1.4012985e-45, + 0.0 + ] + }, + { + "label": "motion-near-zero-vel", + "pos": [ + 3.0, + -2.0, + 5.0 + ], + "vel": [ + 1e-12, + -1e-12, + 1e-12 + ], + "expected_pos": [ + 3.0, + -2.0, + 5.0 + ] + } + ], + "dt": 1.0 +} \ No newline at end of file diff --git a/crates/rmg-core/tests/motion_golden_fixtures.rs b/crates/rmg-core/tests/motion_golden_fixtures.rs index 26a512cb..34d5bf67 100644 --- a/crates/rmg-core/tests/motion_golden_fixtures.rs +++ b/crates/rmg-core/tests/motion_golden_fixtures.rs @@ -1,10 +1,11 @@ #![allow(missing_docs)] +use bytes::Bytes; use once_cell::sync::Lazy; use serde::Deserialize; use rmg_core::{ - decode_motion_payload, encode_motion_payload, make_node_id, make_type_id, ApplyResult, Engine, - GraphStore, NodeRecord, MOTION_RULE_NAME, + build_motion_demo_engine, decode_motion_payload, encode_motion_payload, make_node_id, + make_type_id, ApplyResult, Engine, NodeRecord, MOTION_RULE_NAME, }; static RAW: &str = include_str!("fixtures/motion-fixtures.json"); @@ -28,45 +29,85 @@ static FIXTURES: Lazy = #[test] fn motion_golden_fixtures_apply_as_expected() { let entity_ty = make_type_id("entity"); + let mut engine: Engine = build_motion_demo_engine(); + for case in &FIXTURES.cases { let ent = make_node_id(&case.label); - // Create a fresh engine and insert entity with payload - let mut store = GraphStore::default(); let payload = encode_motion_payload(case.pos, case.vel); - store.insert_node( + engine.insert_node( ent, NodeRecord { ty: entity_ty, payload: Some(payload), }, ); - let mut engine = Engine::new(store, ent); - engine - .register_rule(rmg_core::motion_rule()) - .expect("register motion rule"); let tx = engine.begin(); - let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply"); + let res = engine + .apply(tx, MOTION_RULE_NAME, &ent) + .unwrap_or_else(|_| panic!("apply motion rule failed for case: {}", case.label)); assert!(matches!(res, ApplyResult::Applied)); engine.commit(tx).expect("commit"); - // Verify payload bytes decode to expected values let node = engine.node(&ent).expect("node exists"); let (pos, vel) = decode_motion_payload(node.payload.as_ref().expect("payload")).expect("decode"); - for i in 0..3 { + for (i, v) in vel.iter().enumerate() { assert_eq!( - vel[i].to_bits(), + v.to_bits(), case.vel[i].to_bits(), - "vel component {}", - i + "[{}] velocity[{}] mismatch: got {:?}, expected {:?}", + case.label, + i, + v, + case.vel[i] ); + } + for (i, p) in pos.iter().enumerate() { assert_eq!( - pos[i].to_bits(), + p.to_bits(), case.expected_pos[i].to_bits(), - "pos component {}", - i + "[{}] position[{}] mismatch: got {:?}, expected {:?}", + case.label, + i, + p, + case.expected_pos[i] ); } } } + +#[test] +fn motion_apply_no_payload_returns_nomatch() { + let entity_ty = make_type_id("entity"); + let ent = make_node_id("no-payload"); + let mut engine = build_motion_demo_engine(); + engine.insert_node( + ent, + NodeRecord { + ty: entity_ty, + payload: None, + }, + ); + let tx = engine.begin(); + let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply"); + assert!(matches!(res, ApplyResult::NoMatch)); +} + +#[test] +fn motion_apply_invalid_payload_size_returns_nomatch() { + let entity_ty = make_type_id("entity"); + let ent = make_node_id("bad-payload"); + let mut engine = build_motion_demo_engine(); + let bad = Bytes::from(vec![0u8; 10]); + engine.insert_node( + ent, + NodeRecord { + ty: entity_ty, + payload: Some(bad), + }, + ); + let tx = engine.begin(); + let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply"); + assert!(matches!(res, ApplyResult::NoMatch)); +}