From 97bc2076b13d18b8d4a73b1e2a460999dca37554 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 22 Sep 2026 21:11:35 +0200 Subject: [PATCH] Rust R-AB: vendor the parity fixture too, so cargo-mutants' baseline actually runs The schema_discovery.rs include_str! escape was not the only one: tests/selection.rs's fixture() read ../../contracts/cultnet/interop/selection-vectors.fixture.json, the same three-levels-up path shape, and 37 of 45 tests panicked on it under cargo-mutants' isolated copy - confirmed on Yggdrasil after the schema_discovery.rs fix alone still landed "cargo test failed in an unmutated tree, so no mutants were tested". selection-vectors.fixture.json is a checked-in input (not a generated artifact), so it gets the same treatment as the 22 schemas: a byte-identical vendored copy under packages/cultnet-rs/contracts/cultnet/interop/, a drift test (fixture_contract_matches_the_canonical_source, same skip-when-absent shape as schema_discovery_contracts.rs), and the loader reads the vendored copy unconditionally. selection-vectors.cs-written.json is different in kind: it is the C# reference's own generated output, produced only by a real cross-runtime checkout running SelectionParityVectorTests with CULTNET_WRITE_VECTORS=1. There is nothing to vendor - an isolated Rust-only copy can never satisfy that test's premise regardless of what this crate does with its own files. That one test now skips (with an explanation) when the canonical contracts/ tree is entirely absent, rather than panicking and failing the whole suite over a cross-runtime artifact the isolated copy cannot produce by construction. Verified locally by hiding contracts/ entirely and rerunning the full suite (217 tests across every binary): unchanged pass count, same as with it present. Co-Authored-By: Claude Sonnet 5 --- .../interop/selection-vectors.fixture.json | 58 +++++++++++++++++++ packages/cultnet-rs/tests/selection.rs | 54 ++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/cultnet-rs/contracts/cultnet/interop/selection-vectors.fixture.json diff --git a/packages/cultnet-rs/contracts/cultnet/interop/selection-vectors.fixture.json b/packages/cultnet-rs/contracts/cultnet/interop/selection-vectors.fixture.json new file mode 100644 index 00000000..6d2b0649 --- /dev/null +++ b/packages/cultnet-rs/contracts/cultnet/interop/selection-vectors.fixture.json @@ -0,0 +1,58 @@ +{ + "//": "CultNet typed selection, Cut 1 fix batch (docs/cultnet-selection-cut.md, Self's rulings for the Cut 1 fix batch, R-E and the shared-fixture ruling, 2026-09-22). The one row set both the C# reference and cultnet-rs build their parity vectors from. schemaId is each schema's real SHA-256 content-hash id, exactly as GameCult.Caching.CultDocumentRegistry computes it for the fixture's C# document types (leaf_a=ParityLeafA, leaf_b=ParityLeafB, citer=ParityCiter, tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs) - CultCache.cs's Sha256(semanticFingerprint), which carries no '.vN' suffix of its own; the version is folded into the hash, never appended as text. Both writers read this file instead of building their own fixture rows.", + "//aliases": "Two DIFFERENT alias forms, now deliberate rather than a cross-runtime defect (S-12, 2026-09-22: the Rust alias port made this so - see docs/cultnet-selection-cut.md, 'Self's ruling, 2026-09-22: the C# reference's alias rule is the rule'). Both runtimes resolve a schema alias the same one way: strip a trailing .vN and compare the remainder against the descriptor's declared SchemaName (CultNetSchemaAliasMatching.Matches(candidate, CultDocumentDescriptor), src/GameCult.Networking/CultNetDatabase.cs:74-87; cultnet_rs::selection::schema_alias::matches ports it name-for-name and deletes the hash-stripping overload it used to carry). nameAlias is .v9 and resolves through both runtimes under that one rule. hashAlias is .v1 and is refused as unmatched by both - it is not a wire form either runtime's production call sites ever produce or accept, and it is kept in the fixture specifically to pin that neither runtime resolves it.", + "schemas": { + "leaf_a": { + "schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff", + "nameAlias": "leaf_a.v9", + "hashAlias": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff.v1", + "indexes": [ + { "name": "kind", "numeric": false }, + { "name": "mass", "numeric": true } + ] + }, + "leaf_b": { + "schemaId": "sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42", + "nameAlias": "leaf_b.v9", + "hashAlias": "sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42.v1", + "indexes": [ + { "name": "kind", "numeric": false }, + { "name": "mass", "numeric": true } + ] + }, + "citer": { + "schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc", + "nameAlias": "citer.v9", + "hashAlias": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc.v1", + "roles": [ + { "name": "Design", "targets": ["leaf_a", "leaf_b"] } + ] + } + }, + "rows": [ + { "schema": "leaf_a", "key": "a-lo", "ordinal": 1, "fields": { "kind": "weapon", "mass": "4" } }, + { "schema": "leaf_a", "key": "a-eq", "ordinal": 2, "fields": { "kind": "weapon", "mass": "5" } }, + { "schema": "leaf_b", "key": "b-hi", "ordinal": 3, "fields": { "kind": "armor", "mass": "6" } }, + { "schema": "leaf_a", "key": "shared-1", "ordinal": 4, "fields": { "kind": "shared", "mass": "1" } }, + { "schema": "leaf_b", "key": "shared-2", "ordinal": 5, "fields": { "kind": "shared", "mass": "1" } }, + { "schema": "leaf_a", "key": "shared-3", "ordinal": 6, "fields": { "kind": "shared", "mass": "1" } }, + { + "schema": "citer", + "key": "citer-1", + "ordinal": 7, + "references": [ { "role": "Design", "targetSchema": "leaf_a", "targetKey": "a-eq" } ] + }, + { + "//": "R-D: 2233759.25 is exactly representable as an f32 and sits on a round-to-even tie between two shortest-form spellings; its exact canonical decimal is 2233759.25 either way. kind=tie so no weapon/armor/shared case can sweep it up.", + "schema": "leaf_a", "key": "tie-mass", "ordinal": 8, "fields": { "kind": "tie", "mass": "2233759.25" } + }, + { + "//": "R-C: BMP private-use key (U+E000), same ordinal as the astral row below so the tiebreak - not the ordinal - decides order. kind=unicode, mass=0 so no unfiltered or mass>1 case reaches these.", + "schema": "leaf_a", "key": "-row", "ordinal": 9, "fields": { "kind": "unicode", "mass": "0" } + }, + { + "//": "R-C: astral key (U+1F602, encoded as a surrogate pair whose high half 0xD83D is less than 0xE000 - a UTF-16 code-unit compare sorts this first; code-point order sorts it after the BMP row above).", + "schema": "leaf_a", "key": "😂-row", "ordinal": 9, "fields": { "kind": "unicode", "mass": "0" } + } + ] +} diff --git a/packages/cultnet-rs/tests/selection.rs b/packages/cultnet-rs/tests/selection.rs index ceb2227b..4ef4827f 100644 --- a/packages/cultnet-rs/tests/selection.rs +++ b/packages/cultnet-rs/tests/selection.rs @@ -92,9 +92,22 @@ struct FixtureFile { static FIXTURE: std::sync::OnceLock = std::sync::OnceLock::new(); +// R-AB: `fixture()` used to read only `contracts_dir()` (the canonical, repo-root +// `contracts/cultnet/interop/`), which is outside `packages/cultnet-rs` and escapes any tool that +// copies just this package's directory - the same failure shape `schema_discovery.rs`'s +// `include_str!`s had. `selection-vectors.fixture.json` is a checked-in input, not a generated +// artifact (unlike `*-written.json` below, which each runtime produces from the *other* runtime's +// output and cannot be vendored the same way), so it gets the same vendored-copy treatment: a +// byte-identical copy lives under `packages/cultnet-rs/contracts/cultnet/interop/`, checked by the +// drift test right below, and the loader reads the vendored copy unconditionally so it works the +// same way whether or not the wider repo is checked out beside the package. +fn vendored_fixture_dir() -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("contracts").join("cultnet").join("interop") +} + fn fixture() -> &'static FixtureFile { FIXTURE.get_or_init(|| { - let path = contracts_dir().join("selection-vectors.fixture.json"); + let path = vendored_fixture_dir().join("selection-vectors.fixture.json"); let text = fs::read_to_string(&path) .unwrap_or_else(|error| panic!("{} is missing or unreadable: {error}", path.display())); serde_json::from_str(&text) @@ -102,6 +115,31 @@ fn fixture() -> &'static FixtureFile { }) } +// The drift guard for the vendored fixture, mirroring +// `schema_discovery_contracts.rs`'s vendored-schema check: skips (not fails) when the canonical +// repo-root tree is not present beside the package, which is exactly the isolated-copy case the +// vendored copy exists to survive. +#[test] +fn fixture_contract_matches_the_canonical_source() { + let canonical = contracts_dir().join("selection-vectors.fixture.json"); + if !canonical.is_file() { + eprintln!( + "skipping: {canonical:?} not present beside the package (isolated build, the case \ + the vendored fixture copy is for)" + ); + return; + } + let canonical_text = fs::read_to_string(&canonical).expect("reading canonical fixture"); + let vendored_text = fs::read_to_string(vendored_fixture_dir().join("selection-vectors.fixture.json")) + .expect("reading vendored fixture"); + assert_eq!( + canonical_text, vendored_text, + "packages/cultnet-rs/contracts/cultnet/interop/selection-vectors.fixture.json has \ + drifted from the canonical contracts/cultnet/interop/selection-vectors.fixture.json - \ + re-copy it" + ); +} + fn fixture_schema(name: &str) -> &'static FixtureSchemaDef { fixture() .schemas @@ -1036,6 +1074,20 @@ fn assert_refusal_vector(vector: &Vector, expected_field: &str) { /// Vectors written by the reference (C#) decode and evaluate identically in Rust. #[test] fn selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust() { + if !contracts_dir().is_dir() { + // R-AB: cs-written.json is the C# reference's own generated output - there is nothing to + // vendor (it is not a checked-in input like the fixture; it exists only once the C# side + // has actually run its writer against a real checkout of both runtimes). An isolated + // Rust-only copy, such as cargo-mutants' baseline build, can never satisfy this test's + // premise no matter what this crate does with its own files, so it skips rather than + // failing the whole suite over a cross-runtime artifact the isolated copy cannot produce. + eprintln!( + "skipping: {:?} not present beside the package (isolated build, no C# checkout to \ + have written cs-written.json against)", + contracts_dir() + ); + return; + } let path = contracts_dir().join("selection-vectors.cs-written.json"); let Ok(text) = fs::read_to_string(&path) else { panic!(