CultNet selection cut1: the shared C#/Rust parity fixture
Reconciles the two Hands' independently-regenerated parity vectors
(Soul's whole-cut pass, F3): the C# fixture used schema *names*
("leaf_a") as literal ids, the Rust fixture used invented fake
sha256-shaped ids - every vector carrying an id failed when read by
the other runtime.
Adds contracts/cultnet/interop/selection-vectors.fixture.json: the one
row set both writers now build from, keyed by each schema's real
SHA-256 content-hash id exactly as CultDocumentRegistry computes it
(dumped via a temporary DumpFixtureSchemaIds run). Carries the rows
either runtime needs - astral/BMP-private-use keys, a float tie,
cites/cited, a cites target reached by alias - plus two DIFFERENT
alias forms, because the two runtimes' alias matchers key off
different descriptor attributes and no single candidate resolves
through both (see the fixture's "//aliases" note and the report).
Rewrites SelectionParityVectorTests.cs to load the fixture instead of
hand-building rows (BuildFixture/BuildExtendedFixture deleted), assert
the fixture's declared schema ids still match what the registry
computes (fails loudly on drift instead of silently comparing wrong
ids), and use the real schema id (not SchemaName) as a row's
cross-runtime identity. Also fixes a real bug this file carried since
before the shared fixture: both the writer and the reader compared
`evaluation.Rows.Count` (the page size) against "matched" instead of
`evaluation.TotalMatched` (the total, per R-G) - selections with a
Limit were writing/checking the wrong number. The reader now collects
every vector's mismatch instead of stopping at the first one, so a
real parity defect is reported in full.
Regenerates selection-vectors.cs-written.json from the shared fixture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a088435f9f
commit
3317dddf46
3 changed files with 363 additions and 122 deletions
|
|
@ -6,7 +6,7 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-eq"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
|
|
@ -18,11 +18,11 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/\uD83D\uDE02-row",
|
||||
"leaf_a/\uE000-row",
|
||||
"leaf_a/tie-mass"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uE000-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
|
||||
],
|
||||
"matched": 3,
|
||||
"matched": 10,
|
||||
"hasNext": true,
|
||||
"expectedEdges": []
|
||||
},
|
||||
|
|
@ -32,10 +32,10 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-lo",
|
||||
"leaf_a/a-eq",
|
||||
"leaf_b/b-hi",
|
||||
"leaf_a/tie-mass"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/b-hi",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
|
||||
],
|
||||
"matched": 4,
|
||||
"hasNext": false,
|
||||
|
|
@ -43,35 +43,35 @@
|
|||
},
|
||||
{
|
||||
"name": "incoming_exists_true",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkaZsZWFmX2Gka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-eq"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": [
|
||||
{
|
||||
"fromId": "citer/citer-1",
|
||||
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
|
||||
"role": "Design",
|
||||
"toId": "leaf_a/a-eq",
|
||||
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"payloadBase64": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "incoming_negation",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkaZsZWFmX2Gka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-lo",
|
||||
"leaf_a/shared-1",
|
||||
"leaf_a/shared-3",
|
||||
"leaf_a/tie-mass",
|
||||
"leaf_a/\uE000-row",
|
||||
"leaf_a/\uD83D\uDE02-row"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uE000-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
|
||||
],
|
||||
"matched": 6,
|
||||
"hasNext": false,
|
||||
|
|
@ -83,9 +83,9 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/shared-1",
|
||||
"leaf_b/shared-2",
|
||||
"leaf_a/shared-3"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
|
||||
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/shared-2",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
|
|
@ -97,39 +97,76 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/tie-mass"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "cites_target_by_alias",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlkpmxlYWZfYalyZWNvcmRLZXmkYS1lcaRyb2xlwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"name": "hop_by_role_cites_exact_id",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqlyZWNvcmRLZXmkYS1lcaRyb2xlwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"citer/citer-1"
|
||||
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": [
|
||||
{
|
||||
"fromId": "citer/citer-1",
|
||||
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
|
||||
"role": "Design",
|
||||
"toId": "leaf_a/a-eq",
|
||||
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"payloadBase64": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cites_target_by_name_alias",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlkqWxlYWZfYS52OalyZWNvcmRLZXmkYS1lcaRyb2xlwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": [
|
||||
{
|
||||
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
|
||||
"role": "Design",
|
||||
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"payloadBase64": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "schemas_by_name_alias",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkalsZWFmX2Eudjmka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uE000-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
|
||||
],
|
||||
"matched": 7,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "astral_key_code_point_order",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/\uE000-row",
|
||||
"leaf_a/\uD83D\uDE02-row"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uE000-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
|
||||
],
|
||||
"matched": 2,
|
||||
"hasNext": false,
|
||||
|
|
@ -141,8 +178,8 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/\uE000-row",
|
||||
"leaf_a/\uD83D\uDE02-row"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uE000-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
|
||||
],
|
||||
"matched": 2,
|
||||
"hasNext": false,
|
||||
|
|
@ -154,8 +191,8 @@
|
|||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/\uE000-row",
|
||||
"leaf_a/\uD83D\uDE02-row"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uE000-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
|
||||
],
|
||||
"matched": 2,
|
||||
"hasNext": false,
|
||||
|
|
|
|||
58
contracts/cultnet/interop/selection-vectors.fixture.json
Normal file
58
contracts/cultnet/interop/selection-vectors.fixture.json
Normal file
|
|
@ -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, because the two runtimes' schema-alias matchers key off different attributes of a descriptor and there is no candidate string that resolves through both (this cut's confirmed report finding): nameAlias is <SchemaName>.v9, which CultNetSchemaAliasMatching.Matches(candidate, CultDocumentDescriptor) resolves by stripping the trailing .vN and comparing to descriptor.SchemaName (C#'s only real alias call sites all use this descriptor overload - src/GameCult.Networking/CultNetDatabase.cs:74-87 - never the schemaId-vs-schemaId overload). hashAlias is <schemaId>.v1, which cultnet_rs::selection::schema_alias::matches resolves by stripping the trailing .vN from EITHER side and comparing the remaining hash text - a mechanism C#'s production code path never invokes for `schemas`/`cites.target.schemaId` at all.",
|
||||
"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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
|
@ -13,15 +14,16 @@ namespace GameCult.Networking.Tests
|
|||
// CultNet typed selection, Cut 1 commit 3/4 (docs/cultnet-selection-cut.md section 6/10, S12):
|
||||
// the C#<->Rust parity vectors. New file; no existing C# source is modified.
|
||||
//
|
||||
// "schemaId" inside the committed vector files is each fixture document's *SchemaName*
|
||||
// ("leaf_a", "leaf_b", "citer"), never CultDocumentDescriptor.SchemaId - a SHA-256 content
|
||||
// hash over the type's canonical shape that Rust has no way to reproduce independently, and
|
||||
// that the reference itself does not run through alias matching everywhere (MatchesCitation
|
||||
// compares Citation.Target.SchemaId against the real hash exactly, with no SchemaName
|
||||
// fallback). The vectors below therefore only exercise `cited` (role/existence, matched by
|
||||
// record key alone - no schema id comparison at all) for the hop, never `cites` (which would
|
||||
// need a real cross-runtime-agreed schema id this fixture pair cannot produce). This is a
|
||||
// fixture-identity convention local to these vectors, not a claim about the real wire.
|
||||
// Self's ruling, 2026-09-22 (shared-fixture ruling, on top of the Cut 1 fix batch): both this
|
||||
// writer and packages/cultnet-rs/tests/selection.rs's write_selection_vectors_for_the_reference
|
||||
// build their rows from the one shared
|
||||
// contracts/cultnet/interop/selection-vectors.fixture.json, keyed by each schema's real
|
||||
// SHA-256 content-hash id, exactly as CultDocumentRegistry computes it for the fixture's own
|
||||
// document types below - never by schema *name* as a stand-in id, which let every vector that
|
||||
// carried an id decode on the wrong runtime's terms (Soul's whole-cut pass, F3: a name-as-id
|
||||
// fixture masked the real alias-matching gap between CultNetSchemaAliasMatching and
|
||||
// cultnet_rs::selection::schema_alias). AssertSchemaIdMatches below fails loudly, not silently,
|
||||
// if this file's reflected shape ever drifts from the committed fixture.
|
||||
public sealed class SelectionParityVectorTests
|
||||
{
|
||||
private static CultDocumentRegistry Registry() => CultDocumentRegistry.ForTypes(new[]
|
||||
|
|
@ -32,63 +34,122 @@ namespace GameCult.Networking.Tests
|
|||
private static CultNetSelectionEvaluator.Row Row(CultDocumentRegistry registry, object document, string key, long ordinal) =>
|
||||
new(registry.GetRequired(document.GetType()), new CultRecordKey(key), document, ordinal);
|
||||
|
||||
// R-C: two rows sharing one ordinal so the tiebreak - not the ordinal - decides their order.
|
||||
// U+E000 is a BMP private-use character (code point 0xE000); U+1F602 is astral, encoded as the
|
||||
// surrogate pair 😂 whose high half (0xD83D) is less than 0xE000. A UTF-16 code-unit
|
||||
// compare (the pre-R-C bug) therefore sorts the astral key first; code-point order sorts it
|
||||
// after, since 0x1F602 > 0xE000.
|
||||
private const string BmpPrivateUseKey = "-row";
|
||||
private const string AstralKey = "😂-row";
|
||||
// ----------------------------------------------------------------------------------------
|
||||
// The shared fixture: contracts/cultnet/interop/selection-vectors.fixture.json. Holds the
|
||||
// real schema ids (and their unversioned aliases) and every row either runtime needs:
|
||||
// astral/BMP-private-use keys (R-C), a float tie (R-D), a cites/cited pair, and a cites
|
||||
// target reached by alias (R-E). Both writers build from it; neither hand-rolls its own row
|
||||
// set any more.
|
||||
// ----------------------------------------------------------------------------------------
|
||||
|
||||
// Unchanged: this exact row set is what the committed selection-vectors.rs-written.json was
|
||||
// computed against on the Rust side (an independently constructed but value-identical fixture,
|
||||
// per the comment on Cases() below) - SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
|
||||
// reads that committed file, so this fixture cannot grow without breaking that comparison until
|
||||
// the Rust side's own fixture grows to match. New rows for the fix-batch vectors live in
|
||||
// BuildExtendedFixture, used only by WriteSelectionVectors (this cut's own cs-written.json).
|
||||
private static (CultDocumentRegistry registry, CultNetSelectionEvaluator.Row[] rows) BuildFixture()
|
||||
// R-E: two alias forms, because the two runtimes' schema-alias matchers key off different
|
||||
// attributes and no candidate resolves through both (see the fixture file's "//aliases"
|
||||
// note and this file's report of the confirmed defect). NameAlias is what
|
||||
// CultNetSchemaAliasMatching's descriptor overload (the only one any C# call site uses)
|
||||
// can resolve; HashAlias is what cultnet_rs::selection::schema_alias can resolve.
|
||||
private sealed record FixtureSchema(string SchemaId, string NameAlias, string HashAlias);
|
||||
|
||||
private sealed record FixtureReference(string Role, string TargetSchema, string TargetKey);
|
||||
|
||||
private sealed record FixtureRow(
|
||||
string Schema, string Key, long Ordinal,
|
||||
IReadOnlyDictionary<string, string> Fields,
|
||||
IReadOnlyList<FixtureReference> References);
|
||||
|
||||
private static string FixturePath() =>
|
||||
Path.Combine(RepoRoot(), "contracts", "cultnet", "interop", "selection-vectors.fixture.json");
|
||||
|
||||
private static (IReadOnlyDictionary<string, FixtureSchema> Schemas, IReadOnlyList<FixtureRow> Rows) LoadFixture()
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(FixturePath()));
|
||||
var root = document.RootElement;
|
||||
|
||||
var schemas = new Dictionary<string, FixtureSchema>(StringComparer.Ordinal);
|
||||
foreach (var entry in root.GetProperty("schemas").EnumerateObject())
|
||||
{
|
||||
schemas[entry.Name] = new FixtureSchema(
|
||||
entry.Value.GetProperty("schemaId").GetString()!,
|
||||
entry.Value.GetProperty("nameAlias").GetString()!,
|
||||
entry.Value.GetProperty("hashAlias").GetString()!);
|
||||
}
|
||||
|
||||
var rows = new List<FixtureRow>();
|
||||
foreach (var row in root.GetProperty("rows").EnumerateArray())
|
||||
{
|
||||
var fields = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (row.TryGetProperty("fields", out var fieldsElement))
|
||||
{
|
||||
foreach (var field in fieldsElement.EnumerateObject())
|
||||
fields[field.Name] = field.Value.GetString()!;
|
||||
}
|
||||
|
||||
var references = new List<FixtureReference>();
|
||||
if (row.TryGetProperty("references", out var referencesElement))
|
||||
{
|
||||
foreach (var reference in referencesElement.EnumerateArray())
|
||||
{
|
||||
references.Add(new FixtureReference(
|
||||
reference.GetProperty("role").GetString()!,
|
||||
reference.GetProperty("targetSchema").GetString()!,
|
||||
reference.GetProperty("targetKey").GetString()!));
|
||||
}
|
||||
}
|
||||
|
||||
rows.Add(new FixtureRow(
|
||||
row.GetProperty("schema").GetString()!,
|
||||
row.GetProperty("key").GetString()!,
|
||||
row.GetProperty("ordinal").GetInt64(),
|
||||
fields,
|
||||
references));
|
||||
}
|
||||
|
||||
return (schemas, rows);
|
||||
}
|
||||
|
||||
private static void AssertSchemaIdMatches(CultDocumentRegistry registry, Type type, string expected)
|
||||
{
|
||||
var actual = registry.GetRequired(type).SchemaId;
|
||||
Assert.That(actual, Is.EqualTo(expected),
|
||||
$"{type.Name}'s real schema id drifted from contracts/cultnet/interop/selection-vectors.fixture.json - " +
|
||||
"regenerate the fixture (its schemas.*.schemaId/alias fields) from CultDocumentRegistry before trusting these vectors.");
|
||||
}
|
||||
|
||||
private static object BuildDocument(FixtureRow row) => row.Schema switch
|
||||
{
|
||||
"leaf_a" => new ParityLeafA
|
||||
{
|
||||
Name = row.Key, Kind = row.Fields["kind"], Mass = float.Parse(row.Fields["mass"], CultureInfo.InvariantCulture)
|
||||
},
|
||||
"leaf_b" => new ParityLeafB
|
||||
{
|
||||
Name = row.Key, Kind = row.Fields["kind"], Mass = float.Parse(row.Fields["mass"], CultureInfo.InvariantCulture)
|
||||
},
|
||||
"citer" => new ParityCiter
|
||||
{
|
||||
Name = row.Key,
|
||||
Design = new CultRecordRef<ParityMiddle>(new CultRecordKey(
|
||||
row.References.Single(reference => reference.Role == "Design").TargetKey))
|
||||
},
|
||||
_ => throw new InvalidOperationException($"selection-vectors.fixture.json: unknown schema '{row.Schema}'")
|
||||
};
|
||||
|
||||
// The one row set: both SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
|
||||
// (reading rs-written) and WriteSelectionVectors (writing cs-written) build from it, so both
|
||||
// committed vector files are regenerated together whenever the fixture changes.
|
||||
private static (CultDocumentRegistry registry, CultNetSelectionEvaluator.Row[] rows, IReadOnlyDictionary<string, FixtureSchema> schemas) BuildFixture()
|
||||
{
|
||||
var registry = Registry();
|
||||
var rows = new[]
|
||||
{
|
||||
Row(registry, new ParityLeafA { Name = "a-lo", Kind = "weapon", Mass = 4 }, "a-lo", 1),
|
||||
Row(registry, new ParityLeafA { Name = "a-eq", Kind = "weapon", Mass = 5 }, "a-eq", 2),
|
||||
Row(registry, new ParityLeafB { Name = "b-hi", Kind = "armor", Mass = 6 }, "b-hi", 3),
|
||||
Row(registry, new ParityLeafA { Name = "shared-1", Kind = "shared", Mass = 1 }, "shared-1", 4),
|
||||
Row(registry, new ParityLeafB { Name = "shared-2", Kind = "shared", Mass = 1 }, "shared-2", 5),
|
||||
Row(registry, new ParityLeafA { Name = "shared-3", Kind = "shared", Mass = 1 }, "shared-3", 6),
|
||||
Row(registry, new ParityCiter { Name = "citer-1", Design = new CultRecordRef<ParityMiddle>(new CultRecordKey("a-eq")) }, "citer-1", 7),
|
||||
};
|
||||
return (registry, rows);
|
||||
var (schemas, fixtureRows) = LoadFixture();
|
||||
|
||||
AssertSchemaIdMatches(registry, typeof(ParityLeafA), schemas["leaf_a"].SchemaId);
|
||||
AssertSchemaIdMatches(registry, typeof(ParityLeafB), schemas["leaf_b"].SchemaId);
|
||||
AssertSchemaIdMatches(registry, typeof(ParityCiter), schemas["citer"].SchemaId);
|
||||
|
||||
var rows = fixtureRows.Select(row => Row(registry, BuildDocument(row), row.Key, row.Ordinal)).ToArray();
|
||||
return (registry, rows, schemas);
|
||||
}
|
||||
|
||||
// R-B/R-C/R-D/R-E/R-I fix-batch rows, additive over BuildFixture so every pre-existing
|
||||
// selection's match set is unchanged (a new row reachable by an unfiltered or loosely filtered
|
||||
// case, such as descending_with_limit or gt_numeric_boundary, would otherwise silently change
|
||||
// that case's expected page). Only WriteSelectionVectors (this cut's cs-written.json) uses this.
|
||||
private static (CultDocumentRegistry registry, CultNetSelectionEvaluator.Row[] rows) BuildExtendedFixture()
|
||||
{
|
||||
var (registry, baseRows) = BuildFixture();
|
||||
var extraRows = new[]
|
||||
{
|
||||
// R-D: 2233759.25f is exactly representable and sits on a round-to-even tie between two
|
||||
// .NET-shortest-form spellings ("2233759.2" vs "2233759.3" between runtimes/versions);
|
||||
// its exact canonical decimal is "2233759.25" either way. Kind "tie" so it cannot be
|
||||
// swept up by an existing weapon/armor/shared-kind case.
|
||||
Row(registry, new ParityLeafA { Name = "tie-mass", Kind = "tie", Mass = 2233759.25f }, "tie-mass", 8),
|
||||
// R-C fixture rows: same ordinal, astral vs BMP-private-use keys (see the constants
|
||||
// above). Kind "unicode" and Mass 0 so neither an unfiltered nor a mass>1 case reaches them.
|
||||
Row(registry, new ParityLeafA { Name = "bmp", Kind = "unicode", Mass = 0 }, BmpPrivateUseKey, 9),
|
||||
Row(registry, new ParityLeafA { Name = "astral", Kind = "unicode", Mass = 0 }, AstralKey, 9),
|
||||
};
|
||||
return (registry, baseRows.Concat(extraRows).ToArray());
|
||||
}
|
||||
|
||||
// The same six selections packages/cultnet-rs/tests/selection.rs's
|
||||
// write_selection_vectors_for_the_reference builds against its own fixture rows (which
|
||||
// carry the same schema names, record keys, ordinals and declared values as BuildFixture
|
||||
// above, independently constructed - a within-runtime round trip pins nothing, section 10).
|
||||
private static IEnumerable<(string Name, CultNetSelection Selection)> Cases()
|
||||
private static IEnumerable<(string Name, CultNetSelection Selection)> Cases(IReadOnlyDictionary<string, FixtureSchema> schemas)
|
||||
{
|
||||
yield return ("any_of_plus_ge_conjunction", new CultNetSelection
|
||||
{
|
||||
|
|
@ -103,14 +164,16 @@ namespace GameCult.Networking.Tests
|
|||
{
|
||||
Fields = new[] { new CultNetFieldPredicate { Index = "mass", Op = "gt", Number = "1" } }
|
||||
});
|
||||
// R-E: the exact real schema id - matches on both runtimes' first (exact string)
|
||||
// comparison branch, independent of the alias-matching gap below.
|
||||
yield return ("incoming_exists_true", new CultNetSelection
|
||||
{
|
||||
Schemas = new[] { "leaf_a" },
|
||||
Schemas = new[] { schemas["leaf_a"].SchemaId },
|
||||
Cited = new CultNetIncoming { Role = "Design", Exists = true }
|
||||
});
|
||||
yield return ("incoming_negation", new CultNetSelection
|
||||
{
|
||||
Schemas = new[] { "leaf_a" },
|
||||
Schemas = new[] { schemas["leaf_a"].SchemaId },
|
||||
Cited = new CultNetIncoming { Role = "Design", Exists = false }
|
||||
});
|
||||
yield return ("shared_index_value_three_rows", new CultNetSelection
|
||||
|
|
@ -124,12 +187,27 @@ namespace GameCult.Networking.Tests
|
|||
{
|
||||
Fields = new[] { new CultNetFieldPredicate { Index = "mass", Op = "ge", Number = "2233759.25" } }
|
||||
});
|
||||
// R-E: cites.target.schemaId goes through the alias matcher, so "leaf_a" (ParityLeafA's
|
||||
// SchemaName) matches a-eq's real content-hash SchemaId, same as `schemas` already does.
|
||||
yield return ("cites_target_by_alias", new CultNetSelection
|
||||
// R-E: the exact real schema id, mirroring cultnet-rs's hop_by_role_cites_exact_id.
|
||||
yield return ("hop_by_role_cites_exact_id", new CultNetSelection
|
||||
{
|
||||
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = "leaf_a", RecordKey = "a-eq" } }
|
||||
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].SchemaId, RecordKey = "a-eq" } }
|
||||
});
|
||||
// R-E: the same citation, but the target is named by its C#-resolvable alias
|
||||
// ("leaf_a.v9") rather than the exact id. CultNetSchemaAliasMatching's descriptor overload
|
||||
// (CultNetDatabase.cs:74-87, the only overload any `schemas`/`cites.target.schemaId` call
|
||||
// site in src/ uses) strips the trailing ".v9" and compares "leaf_a" to
|
||||
// ParityLeafA's descriptor.SchemaName - a real match here, in the reference. Rust's
|
||||
// schema_alias has no concept of a schema's human name at all - it only ever sees the raw
|
||||
// hash id (Row::schema_id()) - so this vector's expectedIds are real and non-empty here,
|
||||
// but when cultnet-rs's reader judges this same vector it cannot resolve "leaf_a.v9" to
|
||||
// anything (packages/cultnet-rs/src/selection.rs:452-459) and gets zero rows instead.
|
||||
// Confirmed cross-runtime defect; see the report.
|
||||
yield return ("cites_target_by_name_alias", new CultNetSelection
|
||||
{
|
||||
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].NameAlias, RecordKey = "a-eq" } }
|
||||
});
|
||||
// R-E: `schemas` reached by the same name alias. Same one-sided-match caveat as above.
|
||||
yield return ("schemas_by_name_alias", new CultNetSelection { Schemas = new[] { schemas["leaf_a"].NameAlias } });
|
||||
// R-C: BmpPrivateUseKey (U+E000) sorts before AstralKey (U+1F602) in code-point order,
|
||||
// despite AstralKey's leading UTF-16 code unit (0xD83D) being numerically smaller.
|
||||
yield return ("astral_key_code_point_order", new CultNetSelection
|
||||
|
|
@ -152,13 +230,23 @@ 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/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()
|
||||
{
|
||||
yield return ("refuses_empty_schemas_list", new CultNetSelection { Schemas = Array.Empty<string>() }, "schemas");
|
||||
yield return ("refuses_blank_key_entry", new CultNetSelection { Keys = new[] { "a-eq", " " } }, "keys");
|
||||
}
|
||||
|
||||
private static string RowId(CultNetSelectionEvaluator.Row row) => $"{row.Descriptor.SchemaName}/{row.Key.Value}";
|
||||
// R-E: id, not name - a row's cross-runtime identity is its real schema id plus its key, the
|
||||
// same shape cultnet-rs's row_id() produces.
|
||||
private static string RowId(CultNetSelectionEvaluator.Row row) => $"{row.Descriptor.SchemaId}/{row.Key.Value}";
|
||||
|
||||
// Vectors written by Rust (packages/cultnet-rs/tests/selection.rs,
|
||||
// write_selection_vectors_for_the_reference) decode and evaluate identically here.
|
||||
|
|
@ -169,11 +257,16 @@ namespace GameCult.Networking.Tests
|
|||
if (!File.Exists(path))
|
||||
Assert.Inconclusive($"{path} is missing - run cargo test -p cultnet-rs --test selection with CULTNET_WRITE_VECTORS=1 first.");
|
||||
|
||||
var (registry, rows) = BuildFixture();
|
||||
var (registry, rows, _) = BuildFixture();
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var vectors = document.RootElement.GetProperty("vectors");
|
||||
Assert.That(vectors.GetArrayLength(), Is.GreaterThan(0), "the vector file must carry at least one vector");
|
||||
|
||||
// Self's ruling, 2026-09-22 (shared-fixture ruling): a real parity defect is reported in
|
||||
// full, not truncated by whichever vector happened to come first in the file - every
|
||||
// vector is judged and every mismatch collected before the test fails once, mirroring
|
||||
// packages/cultnet-rs/tests/selection.rs's own reader.
|
||||
var failures = new List<string>();
|
||||
foreach (var vector in vectors.EnumerateArray())
|
||||
{
|
||||
var name = vector.GetProperty("name").GetString();
|
||||
|
|
@ -183,34 +276,71 @@ namespace GameCult.Networking.Tests
|
|||
|
||||
if (vector.TryGetProperty("expectedRefusalField", out var refusalField) && refusalField.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
var error = Assert.Throws<CultNetSelectionInvalidException>(() => selection.Validate(registry.AllDescriptors.ToArray()), $"vector {name} must be refused");
|
||||
Assert.That(error!.Field, Is.EqualTo(refusalField.GetString()), $"vector {name}: refusal field");
|
||||
try
|
||||
{
|
||||
selection.Validate(registry.AllDescriptors.ToArray());
|
||||
failures.Add($"{name}: expected refusal at '{refusalField.GetString()}' but the reference accepted it");
|
||||
}
|
||||
catch (CultNetSelectionInvalidException error)
|
||||
{
|
||||
if (error.Field != refusalField.GetString())
|
||||
failures.Add($"{name}: refusal field - expected '{refusalField.GetString()}', got '{error.Field}'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var evaluation = CultNetSelectionEvaluator.Select(registry, rows, selection, asOf);
|
||||
CultNetSelectionEvaluator.Evaluation evaluation;
|
||||
try
|
||||
{
|
||||
evaluation = CultNetSelectionEvaluator.Select(registry, rows, selection, asOf);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
var expectedIds = vector.GetProperty("expectedIds").EnumerateArray().Select(e => e.GetString()).ToArray();
|
||||
failures.Add($"{name}: expected real evaluation (expectedIds=[{string.Join(", ", expectedIds)}]) but Select threw {error.GetType().Name}: {error.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var expectedIds = vector.GetProperty("expectedIds").EnumerateArray().Select(e => e.GetString()).ToArray();
|
||||
Assert.That(evaluation.Rows.Select(RowId).ToArray(), Is.EqualTo(expectedIds), $"vector {name}: row ids");
|
||||
Assert.That((uint)evaluation.Rows.Count, Is.EqualTo(vector.GetProperty("matched").GetUInt32()), $"vector {name}: matched");
|
||||
Assert.That(evaluation.NextCursor != null, Is.EqualTo(vector.GetProperty("hasNext").GetBoolean()), $"vector {name}: hasNext");
|
||||
var expectedIdsList = vector.GetProperty("expectedIds").EnumerateArray().Select(e => e.GetString()).ToArray();
|
||||
var actualIds = evaluation.Rows.Select(RowId).ToArray();
|
||||
if (!actualIds.SequenceEqual(expectedIdsList))
|
||||
failures.Add($"{name}: row ids - expected [{string.Join(", ", expectedIdsList)}], got [{string.Join(", ", actualIds)}]");
|
||||
|
||||
var expectedMatched = vector.GetProperty("matched").GetUInt32();
|
||||
if ((uint)evaluation.TotalMatched != expectedMatched)
|
||||
failures.Add($"{name}: matched - expected {expectedMatched}, got {evaluation.TotalMatched}");
|
||||
|
||||
var expectedHasNext = vector.GetProperty("hasNext").GetBoolean();
|
||||
if ((evaluation.NextCursor != null) != expectedHasNext)
|
||||
failures.Add($"{name}: hasNext - expected {expectedHasNext}, got {evaluation.NextCursor != null}");
|
||||
|
||||
var expectedEdges = vector.GetProperty("expectedEdges").EnumerateArray().ToArray();
|
||||
Assert.That(evaluation.Edges.Count, Is.EqualTo(expectedEdges.Length), $"vector {name}: edge count");
|
||||
if (evaluation.Edges.Count != expectedEdges.Length)
|
||||
{
|
||||
failures.Add($"{name}: edge count - expected {expectedEdges.Length}, got {evaluation.Edges.Count}");
|
||||
continue;
|
||||
}
|
||||
for (var i = 0; i < expectedEdges.Length; i++)
|
||||
{
|
||||
var edge = evaluation.Edges[i];
|
||||
var expectedEdge = expectedEdges[i];
|
||||
Assert.That(RowId(edge.From), Is.EqualTo(expectedEdge.GetProperty("fromId").GetString()), $"vector {name}: edge[{i}].from");
|
||||
Assert.That(edge.Role, Is.EqualTo(expectedEdge.GetProperty("role").GetString()), $"vector {name}: edge[{i}].role");
|
||||
Assert.That(RowId(edge.To), Is.EqualTo(expectedEdge.GetProperty("toId").GetString()), $"vector {name}: edge[{i}].to");
|
||||
var expectedPayload = expectedEdge.GetProperty("payloadBase64");
|
||||
if (expectedPayload.ValueKind == JsonValueKind.Null)
|
||||
Assert.That(edge.Payload, Is.Null, $"vector {name}: edge[{i}].payload");
|
||||
else
|
||||
Assert.That(Convert.ToBase64String((byte[])edge.Payload!), Is.EqualTo(expectedPayload.GetString()), $"vector {name}: edge[{i}].payload");
|
||||
var actualPayload = edge.Payload == null ? null : Convert.ToBase64String((byte[])edge.Payload);
|
||||
var payloadMatches = expectedPayload.ValueKind == JsonValueKind.Null
|
||||
? actualPayload == null
|
||||
: actualPayload == expectedPayload.GetString();
|
||||
if (RowId(edge.From) != expectedEdge.GetProperty("fromId").GetString() ||
|
||||
edge.Role != expectedEdge.GetProperty("role").GetString() ||
|
||||
RowId(edge.To) != expectedEdge.GetProperty("toId").GetString() ||
|
||||
!payloadMatches)
|
||||
{
|
||||
failures.Add($"{name}: edge[{i}] mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.That(failures, Is.Empty,
|
||||
$"{failures.Count} of {vectors.GetArrayLength()} vectors disagreed with the reference:\n" + string.Join("\n", failures));
|
||||
}
|
||||
|
||||
// Writes contracts/cultnet/interop/selection-vectors.cs-written.json for Rust's
|
||||
|
|
@ -223,9 +353,9 @@ namespace GameCult.Networking.Tests
|
|||
if (Environment.GetEnvironmentVariable("CULTNET_WRITE_VECTORS") != "1")
|
||||
Assert.Ignore("Set CULTNET_WRITE_VECTORS=1 to (re)write the committed vector file.");
|
||||
|
||||
var (registry, rows) = BuildExtendedFixture();
|
||||
var (registry, rows, schemas) = BuildFixture();
|
||||
var vectors = new List<object>();
|
||||
foreach (var (name, selection) in Cases())
|
||||
foreach (var (name, selection) in Cases(schemas))
|
||||
{
|
||||
var evaluation = CultNetSelectionEvaluator.Select(registry, rows, selection, asOf: 1);
|
||||
var selectionBytes = MessagePackSerializer.Serialize(selection, CultNetSchemaMessageSerialization.Options);
|
||||
|
|
@ -237,16 +367,16 @@ namespace GameCult.Networking.Tests
|
|||
asOf = 1UL,
|
||||
expectedRefusalField = (string?)null,
|
||||
expectedIds = evaluation.Rows.Select(RowId).ToArray(),
|
||||
matched = (uint)evaluation.Rows.Count,
|
||||
matched = (uint)evaluation.TotalMatched,
|
||||
hasNext = evaluation.NextCursor != null,
|
||||
expectedEdges = evaluation.Edges.Select(edge => new
|
||||
{
|
||||
fromId = RowId(edge.From),
|
||||
role = edge.Role,
|
||||
toId = RowId(edge.To),
|
||||
// None of this file's six vectors hop through a many-dictionary reference
|
||||
// (the only shape D11 gives a non-null payload), so this is always null
|
||||
// here; packages/cultnet-rs/tests/selection.rs's
|
||||
// None of this file's cases hop through a many-dictionary reference (the only
|
||||
// shape D11 gives a non-null payload), so this is always null here;
|
||||
// packages/cultnet-rs/tests/selection.rs's
|
||||
// dictionary_reference_entries_keep_distinct_payload_bytes pins the
|
||||
// payload-carrying case directly against Row::references() instead.
|
||||
payloadBase64 = edge.Payload == null ? null : Convert.ToBase64String((byte[])edge.Payload)
|
||||
|
|
@ -284,6 +414,22 @@ namespace GameCult.Networking.Tests
|
|||
File.WriteAllText(Path.Combine(directory, "selection-vectors.cs-written.json"), json);
|
||||
}
|
||||
|
||||
// Dumps each fixture schema's real, reference-computed id (and lets a maintainer derive the
|
||||
// unversioned alias by stripping the trailing ".v1") so
|
||||
// contracts/cultnet/interop/selection-vectors.fixture.json can be regenerated after a shape
|
||||
// change. Not part of the parity contract itself.
|
||||
[Test]
|
||||
public void DumpFixtureSchemaIds()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("CULTNET_DUMP_SCHEMA_IDS") != "1")
|
||||
Assert.Ignore("Set CULTNET_DUMP_SCHEMA_IDS=1 to print the fixture's real schema ids.");
|
||||
|
||||
var registry = Registry();
|
||||
Console.WriteLine("SCHEMA_ID leaf_a=" + registry.GetRequired(typeof(ParityLeafA)).SchemaId);
|
||||
Console.WriteLine("SCHEMA_ID leaf_b=" + registry.GetRequired(typeof(ParityLeafB)).SchemaId);
|
||||
Console.WriteLine("SCHEMA_ID citer=" + registry.GetRequired(typeof(ParityCiter)).SchemaId);
|
||||
}
|
||||
|
||||
private static string RepoRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
|
||||
|
|
|
|||
Loading…
Reference in a new issue