From b1932d95ed65311c368b1a007d013c317dc82893 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 22 Sep 2026 19:22:18 +0200 Subject: [PATCH] Add the two pending cites-refusal parity vectors, now that C# refuses them Item 1 of the fix-batch-3 brief: check whether CultNetSelection.Validate refuses a cites.target.schemaId that CultNetSchemaAliasMatching cannot resolve. It already does (CultNetSelection.cs:389-394), landed by an earlier Hands pass on this branch (the "C# matrix Hands", after the parity table in the spec was captured) - so no production code changed here. What was still missing: SelectionParityVectorTests.RefusalCases() had a comment explaining why refuses_unmatched_cites_target_schema stayed "Rust-only", dated before that door check landed. The comment was stale and the vector was never added. Added it, plus refuses_cites_target_by_hash_shaped_alias (Self's ruling 2026-09-22: "the C# reference's alias rule is the rule" - C# only resolves a name-form alias, so a hash-shaped cites.target.schemaId is exactly the same unresolved-target case and must refuse at the same field). RefusalCases() now takes the fixture's schemas dictionary to reach schemas["leaf_a"].HashAlias. Verified on Yggdrasil: dotnet test tests/GameCult.Networking.Tests -c Release --filter FullyQualifiedName~SelectionParityVectorTests (the existing cross-runtime reader against the committed rs-written.json still passes, so this change does not depend on anything the Rust Hands owns). Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json with CULTNET_WRITE_VECTORS=1: both new refusal vectors are present and both refuse at "cites.target.schemaId" as expected before being committed as vectors (WriteSelectionVectors's own Assert.Throws gate). Co-Authored-By: Claude Sonnet 5 --- .../SelectionParityVectorTests.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs b/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs index bacdc1b0..87ffe933 100644 --- a/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +++ b/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs @@ -231,17 +231,26 @@ namespace GameCult.Networking.Tests // either side - only decoded and handed to Validate/validate, which must refuse at exactly // the named field. Mirrors packages/cultnet-rs/tests/selection.rs's refusal_cases. // - // Not mirrored here: cultnet-rs's refuses_unmatched_cites_target_schema. R-E says an - // unresolved cites.target.schemaId "is refused at the door, never answered with an empty - // page", but CultNetSelection.Validate (CultNetSelection.cs:361-391) never checks that - // cites.target.schemaId resolves to any known schema at all - only that it is present. Adding - // that vector here would make this file's own Assert.Throws fail before a vector could ever be - // written, so it stays Rust-only; the report below covers the resulting cross-runtime failure - // when this file's reader judges it. - private static IEnumerable<(string Name, CultNetSelection Selection, string ExpectedField)> RefusalCases() + // refuses_unmatched_cites_target_schema and refuses_cites_target_by_hash_shaped_alias now + // land here too: the C# matrix Hands pass added the door check + // (CultNetSelection.cs:389-394) that CultNetSelection.Validate refuses a cites.target.schemaId + // that CultNetSchemaAliasMatching cannot resolve to any declared schema. Under Self's ruling + // 2026-09-22 ("the C# reference's alias rule is the rule"), C# only ever resolves a name-form + // alias, never a hash-shaped one, so a hash-shaped cites.target.schemaId is exactly such an + // unresolved target and is refused at the same field. + private static IEnumerable<(string Name, CultNetSelection Selection, string ExpectedField)> RefusalCases( + IReadOnlyDictionary schemas) { yield return ("refuses_empty_schemas_list", new CultNetSelection { Schemas = Array.Empty() }, "schemas"); yield return ("refuses_blank_key_entry", new CultNetSelection { Keys = new[] { "a-eq", " " } }, "keys"); + yield return ("refuses_unmatched_cites_target_schema", new CultNetSelection + { + Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = "sha256:not-a-declared-schema", RecordKey = "a-eq" } } + }, "cites.target.schemaId"); + yield return ("refuses_cites_target_by_hash_shaped_alias", new CultNetSelection + { + Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].HashAlias, RecordKey = "a-eq" } } + }, "cites.target.schemaId"); } // R-E: id, not name - a row's cross-runtime identity is its real schema id plus its key, the @@ -387,7 +396,7 @@ namespace GameCult.Networking.Tests // Self's ruling, 2026-09-22 (commit 2 fix batch): door-refusal vectors. Never // evaluated on either side - only decoded and handed to Validate, which must refuse // at exactly the named field. - foreach (var (name, selection, expectedField) in RefusalCases()) + foreach (var (name, selection, expectedField) in RefusalCases(schemas)) { var error = Assert.Throws( () => selection.Validate(registry.AllDescriptors.ToArray()),