feat(ci): add make fixtures-client target for GDScript->Rust cross-encoder validation (#475)

Closes the bidirectional protocol compatibility loop (D-030 Layer 1):
- GDScript fixture generator (20 fixtures: inputs, boundary ticks, batch)
- Rust decoder test verifying all GDScript-encoded fixtures deserialize
- Makefile target with generation + verification in one step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 02:20:24 +01:00
co-authored by Claude Opus 4.6
parent 58072bc119
commit da63aa580e
24 changed files with 277 additions and 1 deletions
Binary file not shown.
@@ -0,0 +1 @@
うtickヲaction・Pause
@@ -0,0 +1 @@
うtickフ€ヲaction・Pause
@@ -0,0 +1 @@
‚¤tickÒÿÿÿ¦action¥Pause
@@ -0,0 +1 @@
うtickフヲaction・Pause
Binary file not shown.
@@ -0,0 +1 @@
うtickムヲaction・Pause
Binary file not shown.
@@ -0,0 +1 @@
‚¤tickÍÿÿ¦action¥Pause
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
うtickdヲaction→Interactげtarget_entity_idタ、verbタ
@@ -0,0 +1 @@
うtickdヲactionゥMoveNorth
@@ -0,0 +1 @@
うtickdヲactionュMoveNortheast
@@ -0,0 +1 @@
うtickdヲactionュMoveNorthwest
@@ -0,0 +1 @@
うtickdヲactionュMoveSoutheast
@@ -0,0 +1 @@
うtickdヲactionュMoveSouthwest
+1
View File
@@ -0,0 +1 @@
うtickdヲaction・Pause
@@ -0,0 +1 @@
うtickフネヲactionUsePerceptionModeァthermal
@@ -0,0 +1 @@
うtickdヲactionョToggleStanceUp
+94
View File
@@ -1004,6 +1004,100 @@ fn malformed_input_in_batch_rejects_entire_batch() {
);
}
/// GDScript-generated fixtures must deserialize correctly (D-030 Layer 1, #475).
/// Validates the reverse direction: GDScript encoder -> Rust decoder.
/// Together with all_fixtures_deserialize (Rust -> GDScript), this closes the
/// cross-encoder compatibility loop.
///
/// Fixtures generated by: make fixtures-client
/// (runs client/tests/gen_client_fixtures.gd via Godot headless)
#[test]
fn gdscript_generated_fixtures_deserialize() {
let fixture_dir = std::path::Path::new("tests/fixtures/gdscript");
if !fixture_dir.exists() {
eprintln!(
"SKIP: GDScript fixtures not found at {}. Run `make fixtures-client` to generate.",
fixture_dir.display()
);
return;
}
let mut count = 0;
for entry in fs::read_dir(&fixture_dir).expect("read gdscript fixture dir") {
let entry = entry.expect("read dir entry");
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("msgpack") {
continue;
}
let name = path.file_stem().unwrap().to_str().unwrap().to_string();
let bytes = fs::read(&path).unwrap_or_else(|_| panic!("read fixture {}", name));
if name.starts_with("input_batch") {
let inputs: Vec<PlayerInput> = rmp_serde::from_slice(&bytes).unwrap_or_else(|e| {
panic!("deserialize GDScript batch fixture {}: {}", name, e)
});
assert!(
!inputs.is_empty(),
"batch fixture {} should not be empty",
name
);
} else if name.starts_with("input_") || name.starts_with("boundary_tick_") {
let input: PlayerInput = rmp_serde::from_slice(&bytes).unwrap_or_else(|e| {
panic!("deserialize GDScript input fixture {}: {}", name, e)
});
// Verify specific fixtures for extra confidence
match name.as_str() {
"input_move_north" => {
assert_eq!(input.tick, 100);
assert!(matches!(input.action, PlayerAction::MoveNorth));
}
"input_perception_mode" => {
assert_eq!(input.tick, 200);
assert!(
matches!(input.action, PlayerAction::UsePerceptionMode(ref s) if s == "thermal")
);
}
"input_interact" => {
assert_eq!(input.tick, 100);
assert!(matches!(input.action, PlayerAction::Interact { .. }));
}
"boundary_tick_256" => {
assert_eq!(input.tick, 256, "int_16 asymmetry: tick=256");
}
"boundary_tick_32767" => {
assert_eq!(input.tick, 32767, "int_16 asymmetry: tick=32767");
}
"boundary_tick_65536" => {
assert_eq!(input.tick, 65536, "int_32 asymmetry: tick=65536");
}
"boundary_tick_2147483647" => {
assert_eq!(
input.tick, 2147483647,
"int_32 asymmetry: tick=2^31-1"
);
}
_ => {} // Other fixtures: deserialization success is sufficient
}
} else {
panic!(
"unknown GDScript fixture naming convention: {} — add a deserialization branch",
name
);
}
count += 1;
}
if count == 0 {
eprintln!(
"SKIP: no .msgpack files found in {}. Run `make fixtures-client` to generate.",
fixture_dir.display()
);
return;
}
eprintln!("Verified {} GDScript-generated fixtures", count);
}
/// NearbyInteraction.object_type round-trips through MessagePack (#422).
/// Verifies object_type=Some(Container) survives the wire.
#[test]