docs(@projects/@magic-civilization): 📝 clarify marshaling contract for golden test vectors
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
fb93ed808d
commit
0b7fa3b706
1 changed files with 28 additions and 0 deletions
|
|
@ -81,6 +81,34 @@ Minimum **3 vectors per domain** (happy path, edge case, stress):
|
|||
4. GDExt consumer: add a case in `src/game/engine/tests/ffi/test_golden_<domain>.gd`.
|
||||
5. Run `./run test:golden` — all three must pass. If any diverge, fix the marshaling / determinism / SOT bug first.
|
||||
|
||||
## Cross-consumer marshaling contract (IMPORTANT — affects WASM and GDExt consumers)
|
||||
|
||||
JSON numbers parse as `f64` / `number` in JavaScript and in `JSON.parse_string` in GDScript. Rust's FFI surfaces (`#[wasm_bindgen]` for WASM, `#[godot_api]` / `Variant::try_to::<i64>()` for GDExt) expect specific integer widths for fields declared as `i32` / `i64` / `u32` on the Rust side. **Passing a float where an int is expected causes silent zero-default substitution on the GDExt path** (Godot's `Variant` coercion returns `Err` and godot-rust's default handler uses zeroes instead of raising). That manifests as "every output field is zero/true" instead of the actual compute — a class of bug that bypasses any `assert_eq!` because the shape still matches.
|
||||
|
||||
**Contract for all consumers of every vector:**
|
||||
|
||||
| Fixture field type | Rust consumer | WASM consumer (TS) | GDExt consumer (GDScript) |
|
||||
|---|---|---|---|
|
||||
| `i32` / `i64` / `u32` field | deserialize direct via serde | `Number.isInteger(v)` guard + cast to `number` (fine — TS numbers are f64 but WASM marshaling accepts `number` for `i32`/`i64`) | `int(raw)` cast **required** before crossing FFI to prevent silent zero-default |
|
||||
| `f32` / `f64` field | deserialize direct | direct | direct |
|
||||
| `bool` field | direct | direct | direct |
|
||||
| String-tagged enum | `#[serde(tag = "...")]` derive | string literal compare | string compare + `match`/`if` ladder |
|
||||
|
||||
**GDScript helper pattern** (established by `gdext-consumer` in `src/game/engine/tests/ffi/golden_vector_loader.gd`):
|
||||
```gdscript
|
||||
static func coerce_ints(d: Dictionary, int_keys: Array[String]) -> Dictionary:
|
||||
var out: Dictionary = d.duplicate()
|
||||
for k in int_keys:
|
||||
if out.has(k):
|
||||
out[k] = int(out[k]) # int() truncates float toward zero
|
||||
return out
|
||||
```
|
||||
Apply this to every Dictionary crossing the FFI boundary into a GDExt method that expects integer fields.
|
||||
|
||||
**TS/WASM consumer** currently does NOT need the coercion because `wasm-bindgen`'s generated bindings handle `number → i32/i64` conversion at the FFI boundary automatically. The WASM consumer just needs to ensure the input Object literal matches the declared param types in `api-wasm/src/lib.rs`. **But note**: if the fixture sets an integer field as `"hp": 10.0` (JSON with decimal point), Vitest's JSON.parse will store `10.0` as JS `number` (identical to `10`), and `wasm-bindgen` will marshal it cleanly. So as long as fixtures don't use `.5` values for integer-typed fields, the asymmetry is benign on the WASM side. Fixture authors MUST NOT put fractional values in integer-typed fields.
|
||||
|
||||
**Fixture convention**: integer-typed fields should be written without a decimal point (`"hp": 10`, not `"hp": 10.0`). Rust's serde will accept both; GDScript's `JSON.parse_string` normalizes both to float; but keeping the convention reduces review friction.
|
||||
|
||||
## Determinism rules
|
||||
|
||||
- All randomness MUST take an explicit `seed` parameter; no wall-clock time, no `SystemTime::now()`.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue