CultNet selection cut1, commit 4: the parity vectors, and the ruling this branch landed under
Two things landed on this branch while commit 3 was in flight, both addressed
here rather than left for the vectors to paper over:
**Self's ruling, 2026-09-22 (commit 2 fix batch, 568e8e3/021e3f1): empty
lists, blank entries, and v0's own cleaning.** Rust already refused an empty
schemas/keys list (inherited from before the ruling); it now also matches
the reference's three further decisions exactly, not just in spirit:
- The door's refusal field is the plain list name ("schemas"/"keys"), not an
indexed "schemas[i]" - selection::validate's SelectionInvalid.field now
matches CultNetSelectionInvalidException("schemas", schema, ...) literally,
because the parity vectors compare this string across runtimes.
- snapshot_query.rs's lower_v0_list now deduplicates a mixed list the same
way CultNetV0SelectionLowering.Lower does (Distinct(StringComparer.Ordinal)
- order-preserving, first occurrence wins), not just blank-filters it.
A new test (serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter,
extended) pins the dedup case directly.
- Two mutation entries (RS-EmptySchemas-Revert, RS-EmptyKeys-Revert) added
for the door's original empty-list refusal, which had tests but no
mutation coverage before this commit.
**S12: the two-way parity vectors**, contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json,
eight vectors each: the six evaluated selections from commit 3 (any_of+ge
conjunction, descending+limit, gt boundary, cited exists true/false, three
rows sharing one index value) plus two door-refusal vectors added for this
ruling - `refuses_empty_schemas_list` (schemas: []) and `refuses_blank_key_entry`
(keys: ["a-eq", " "]) - which decode their selection bytes and assert
`validate`/`Validate` refuses at exactly the named field, never evaluating.
The blank-key vector could not be generated before this commit: the door
rule it exercises did not exist in the reference until 568e8e3 landed.
`cites`-based hop vectors are deliberately absent (documented in both test
files): MatchesCitation compares Citation.Target.SchemaId against the
reference's real SHA-256 schema id exactly, with no SchemaName fallback,
and this fixture pair has no way to agree on that hash across runtimes;
`cited` (role/existence, matched by record key alone) is the hop direction
both sides can cross-check, so incoming_exists_true/incoming_negation carry
that coverage instead. Both reader tests pass:
SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference (C#)
and selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust
(Rust), each judging the other runtime's file.
**The remaining mutation matrix.** Of the coordinator's named set (S1, S2,
S6, S9-S15, S17, S20, S23): S1 was already covered (RS-EmptySchemas/Keys,
RS-BlankSchema/Key); S2 (any_of matches membership, RS-S2-Revert) and S6
(cites.role filters the hop, RS-S6-Revert) are newly covered - S6 needed a
new fixture (cites_role_excludes_a_second_reference_at_the_same_target)
because base_rows()'s citer-1 only ever carries one reference, so a mutant
dropping the role filter had nothing else to wrongly match and survived
against the original hops_one_edge_by_declared_role test; that finding is
recorded in-line as a comment, not silently swapped for an easier mutant.
S9-S15 (subscription reconcile, v0-lowering-of-subscribe) do not apply:
D7 - no Rust caller subscribes in this cut, so there is no subscription
client to mutate. S17 (inherited alias reaches every leaf) does not apply
either: Rust's toy Row/RowSet has no inheritance to begin with - both
leaves in the fixture declare "mass" independently, which is the Rust
spelling of the same guarantee, already exercised throughout the suite
without a dedicated mutant (there is no shared base-type discovery path to
revert). S20 (edges only for a hopping selection) was investigated and
found structurally vacuous in Rust as built: `edges` is only ever populated
inside the cites/cited branches, so a mutant on the has_hop() gate cannot
produce a different observable result for a non-hopping selection - no
entry added; a real gap would need the evaluator's structure to change
first. S23 (matches every row sharing an index value, never a cache's
unique-index winner) has no revertable bug surface: Rust's Row/RowSet
never had a unique-index map to begin with (D7), so this is pinned by
construction, not by a mutation-testable rule; matches_every_row_sharing_an_index_value
still exists as a positive test naming it.
**A pre-existing, unrelated flaky test, found and worked around, not
fixed.** tests/cultnet.rs's reactive_document_coalesces_direct_same_schema_alias_member_writes
fails consistently when the mutation harness runs the full tests/cultnet.rs
binary (both default-parallel and RUST_TEST_THREADS=1), but passes 5/5 in
isolation and passes in a plain `cargo test -p cultnet-rs` run - state
pollution from another test in that binary under the harness's specific
invocation path, not a race this campaign's code participates in (nothing
here touches reactive_document machinery). RS-V0Lowering-Revert's Command
is narrowed to `cargo test --test cultnet serve_read_only_raw_snapshot`
(filtering both M0 and the entry's own run to this campaign's test) so the
suite does not depend on that unrelated test's health; the flake itself is
reported here as a finding, not patched.
Tests: cultnet-rs 174 passed, 0 failed (default parallel run).
GameCult.Networking.Tests 196 passed, 1 skipped (WriteSelectionVectors,
env-gated), 0 failed.
Mutation harness: 24 entries (was 22 in commit 3; +RS-S2-Revert, +RS-S6-Revert;
RS-BlankSchema/BlankKey and RS-V0Lowering-Revert's anchors and field-name
assertions updated to match the landed fix batch's exact spelling), all
KILLED, M0 green.
Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty.
Structural delta, this commit only: 6 files changed, +108/-23 (selection.rs
+15/-9 field-name fix; snapshot_query.rs +25/-14 dedup; tests/cultnet.rs +21
dedup test; tests/selection.rs +35/-2 S6 fixture and field-name fixes;
scripts/mutate-cultnet-selection-rust.entries.ps1 +33/-4 two new entries
plus three corrected anchors/commands; SelectionParityVectorTests.cs +2/-1
field name), plus the two committed vector JSON files (~90 lines each, new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
021e3f1bba
commit
00f4c02dd7
8 changed files with 310 additions and 23 deletions
104
contracts/cultnet/interop/selection-vectors.cs-written.json
Normal file
104
contracts/cultnet/interop/selection-vectors.cs-written.json
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"vectors": [
|
||||
{
|
||||
"name": "any_of_plus_ge_conjunction",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHOShKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpndlYXBvbqZudW1iZXLAhKVpbmRleKRtYXNzom9womdlpnZhbHVlc8CmbnVtYmVyoTWlY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "descending_with_limit",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8OlbGltaXQDpmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"citer/citer-1",
|
||||
"leaf_a/shared-3",
|
||||
"leaf_b/shared-2"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": true,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "gt_numeric_boundary",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRtYXNzom9womd0pnZhbHVlc8CmbnVtYmVyoTGlY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-lo",
|
||||
"leaf_a/a-eq",
|
||||
"leaf_b/b-hi"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "incoming_exists_true",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkaZsZWFmX2Gka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "incoming_negation",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkaZsZWFmX2Gka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-lo",
|
||||
"leaf_a/shared-1",
|
||||
"leaf_a/shared-3"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "shared_index_value_three_rows",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpnNoYXJlZKZudW1iZXLApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": null,
|
||||
"expectedIds": [
|
||||
"leaf_a/shared-1",
|
||||
"leaf_b/shared-2",
|
||||
"leaf_a/shared-3"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "refuses_empty_schemas_list",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzkKRrZXlzwKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": "schemas",
|
||||
"expectedIds": [],
|
||||
"matched": 0,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "refuses_blank_key_entry",
|
||||
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkqRhLWVxoyAgIKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": "keys",
|
||||
"expectedIds": [],
|
||||
"matched": 0,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
}
|
||||
]
|
||||
}
|
||||
98
contracts/cultnet/interop/selection-vectors.rs-written.json
Normal file
98
contracts/cultnet/interop/selection-vectors.rs-written.json
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"vectors": [
|
||||
{
|
||||
"name": "any_of_plus_ge_conjunction",
|
||||
"selectionMessagePackBase64": "g6ZmaWVsZHOSg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpndlYXBvboOlaW5kZXikbWFzc6JvcKJnZaZudW1iZXKhNapwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "descending_with_limit",
|
||||
"selectionMessagePackBase64": "g6pwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nw6VsaW1pdAM=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"citer/citer-1",
|
||||
"leaf_a/shared-3",
|
||||
"leaf_b/shared-2"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": true,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "gt_numeric_boundary",
|
||||
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRtYXNzom9womd0pm51bWJlcqExqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfC",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-lo",
|
||||
"leaf_a/a-eq",
|
||||
"leaf_b/b-hi"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "incoming_exists_true",
|
||||
"selectionMessagePackBase64": "hKdzY2hlbWFzkaZsZWFmX2GlY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "incoming_negation",
|
||||
"selectionMessagePackBase64": "hKdzY2hlbWFzkaZsZWFmX2GlY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"leaf_a/a-lo",
|
||||
"leaf_a/shared-1",
|
||||
"leaf_a/shared-3"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "shared_index_value_three_rows",
|
||||
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpnNoYXJlZKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"leaf_a/shared-1",
|
||||
"leaf_b/shared-2",
|
||||
"leaf_a/shared-3"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "refuses_empty_schemas_list",
|
||||
"selectionMessagePackBase64": "g6dzY2hlbWFzkKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": "schemas",
|
||||
"expectedIds": [],
|
||||
"matched": 0,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "refuses_blank_key_entry",
|
||||
"selectionMessagePackBase64": "g6RrZXlzkqRhLWVxoyAgIKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
|
||||
"asOf": 1,
|
||||
"expectedRefusalField": "keys",
|
||||
"expectedIds": [],
|
||||
"matched": 0,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -774,24 +774,27 @@ pub fn validate(selection: &Selection, row_set: &impl RowSet) -> Result<(), Sele
|
|||
// `null` (Rust: `None`) means no filter, everywhere - an empty-or-blank list is never
|
||||
// silently treated as "no filter" by the evaluator (that was CultNetSelectionEvaluator.cs's
|
||||
// pre-fix bug, section 2's S2-3 finding, and this Rust evaluator never had it).
|
||||
// Field name matches the C# reference exactly (568e8e3): plain "schemas"/"keys", not an
|
||||
// indexed "schemas[i]" - CultNetSelectionInvalidException("schemas", schema, ...) names the
|
||||
// list, not the position, and the parity vectors compare this string.
|
||||
if let Some(schemas) = &selection.schemas {
|
||||
for (index, schema) in schemas.iter().enumerate() {
|
||||
for schema in schemas {
|
||||
if schema.trim().is_empty() {
|
||||
return Err(SelectionInvalid::new(
|
||||
format!("schemas[{index}]"),
|
||||
"schemas",
|
||||
Some(schema.clone()),
|
||||
format!("schemas[{index}] is empty or whitespace."),
|
||||
"selection.schemas carries an empty or whitespace entry.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(keys) = &selection.keys {
|
||||
for (index, key) in keys.iter().enumerate() {
|
||||
for key in keys {
|
||||
if key.trim().is_empty() {
|
||||
return Err(SelectionInvalid::new(
|
||||
format!("keys[{index}]"),
|
||||
"keys",
|
||||
Some(key.clone()),
|
||||
format!("keys[{index}] is empty or whitespace."),
|
||||
"selection.keys carries an empty or whitespace entry.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,15 +367,20 @@ fn require_non_empty(value: &str, field: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// v0's own cleaning (Self's ruling, 2026-09-22): an absent list, an empty list, or a list made
|
||||
/// only of blank entries all lower to `None` (no filter) - a v0 compatibility rule the lowering
|
||||
/// owns, not a meaning the door or the evaluator carries for v1's own lists (those refuse `[]`
|
||||
/// and any blank entry outright, in `selection::validate`).
|
||||
/// v0's own cleaning, matching the reference's `CultNetV0SelectionLowering.Lower` exactly
|
||||
/// (568e8e3): an absent list, an empty list, or a list made only of blank entries all lower to
|
||||
/// `None` (no filter); a mixed list keeps its non-blank entries, deduplicated (order-preserving,
|
||||
/// first occurrence wins - `Distinct(StringComparer.Ordinal)`'s behaviour). A v0 compatibility
|
||||
/// rule the lowering owns, not a meaning the door or the evaluator carries for v1's own lists
|
||||
/// (those refuse `[]` and any blank entry outright, in `selection::validate`).
|
||||
fn lower_v0_list(list: &Option<Vec<String>>) -> Option<Vec<String>> {
|
||||
match list {
|
||||
Some(values) if !values.is_empty() && values.iter().any(|value| !value.trim().is_empty()) => {
|
||||
Some(values.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
let Some(values) = list else { return None };
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let filtered: Vec<String> = values
|
||||
.iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.filter(|value| seen.insert(value.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
if filtered.is_empty() { None } else { Some(filtered) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2098,6 +2098,27 @@ fn serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_fi
|
|||
);
|
||||
}
|
||||
|
||||
// A mixed list keeps its non-blank entries, deduplicated (matches
|
||||
// CultNetV0SelectionLowering.Lower exactly, 568e8e3: Distinct(StringComparer.Ordinal)).
|
||||
let mixed_request = CultNetMessage::SnapshotRequest {
|
||||
message_id: "req-2".to_string(),
|
||||
schema_ids: Some(vec![
|
||||
"ghostlight.agent-state".to_string(),
|
||||
" ".to_string(),
|
||||
"ghostlight.agent-state".to_string(),
|
||||
]),
|
||||
record_keys: None,
|
||||
};
|
||||
let mixed_response = serve_read_only_raw_snapshot(®istry, &policy, &source, &mixed_request)?;
|
||||
let CultNetMessage::SnapshotResponseRaw { documents, .. } = mixed_response else {
|
||||
panic!("expected a raw snapshot response");
|
||||
};
|
||||
assert_eq!(
|
||||
documents.len(),
|
||||
2,
|
||||
"a mixed list keeps its one distinct non-blank schema id, still selecting both rows of that schema"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,6 +270,35 @@ fn hops_one_edge_by_declared_role() {
|
|||
assert_eq!(evaluation.edges[0].role, "Design");
|
||||
}
|
||||
|
||||
// S6, isolated: a citer with two references at the *same* target under different roles - the
|
||||
// role filter must exclude the one the caller did not ask for. base_rows()'s citer-1 only ever
|
||||
// carries one reference, so a mutant that drops the role filter entirely is invisible there (it
|
||||
// has nothing else to wrongly match); this fixture gives it something to wrongly match.
|
||||
#[test]
|
||||
fn cites_role_excludes_a_second_reference_at_the_same_target() {
|
||||
let rows = vec![
|
||||
FixtureRow::leaf("leaf_a", "a-eq", 1, "weapon", "5"),
|
||||
FixtureRow::citer(
|
||||
"double-citer",
|
||||
2,
|
||||
vec![
|
||||
("Design".to_string(), rr("leaf_a", "a-eq"), None),
|
||||
("OtherRole".to_string(), rr("leaf_a", "a-eq"), None),
|
||||
],
|
||||
),
|
||||
];
|
||||
let selection = Selection {
|
||||
cites: Some(Citation {
|
||||
target: rr("leaf_a", "a-eq"),
|
||||
role: Some("Design".into()),
|
||||
}),
|
||||
..Selection::default()
|
||||
};
|
||||
let evaluation = select(&FixtureRowSet, &rows, &selection, 1).unwrap();
|
||||
assert_eq!(evaluation.edges.len(), 1, "OtherRole's reference must not also match");
|
||||
assert_eq!(evaluation.edges[0].role, "Design");
|
||||
}
|
||||
|
||||
// S7: cited { exists } is the one negation, and the two directions answer opposite sets.
|
||||
#[test]
|
||||
fn cited_exists_is_the_one_negation() {
|
||||
|
|
@ -484,13 +513,13 @@ fn validation_refuses_a_blank_entry_in_schemas_or_keys() {
|
|||
schemas: Some(vec!["leaf_a".into(), " ".into()]),
|
||||
..Selection::default()
|
||||
};
|
||||
assert_eq!(validate(&blank_schema, &FixtureRowSet).unwrap_err().field, "schemas[1]");
|
||||
assert_eq!(validate(&blank_schema, &FixtureRowSet).unwrap_err().field, "schemas");
|
||||
|
||||
let blank_key = Selection {
|
||||
keys: Some(vec!["".into()]),
|
||||
..Selection::default()
|
||||
};
|
||||
assert_eq!(validate(&blank_key, &FixtureRowSet).unwrap_err().field, "keys[0]");
|
||||
assert_eq!(validate(&blank_key, &FixtureRowSet).unwrap_err().field, "keys");
|
||||
}
|
||||
|
||||
// Q-J: the door refuses every named non-canonical spelling of a comparison number.
|
||||
|
|
@ -854,7 +883,7 @@ fn write_selection_vectors_for_the_reference() {
|
|||
keys: Some(vec!["a-eq".into(), " ".into()]),
|
||||
..Selection::default()
|
||||
},
|
||||
"keys[1]",
|
||||
"keys",
|
||||
),
|
||||
];
|
||||
for (name, selection, expected_field) in refusal_cases {
|
||||
|
|
|
|||
|
|
@ -184,6 +184,26 @@
|
|||
}"
|
||||
New = 'return value.to_string();'
|
||||
},
|
||||
@{
|
||||
Id = 'RS-S2-Revert'
|
||||
Rule = 'S2: an any_of predicate matches row-value membership, not "any value at all".'
|
||||
Test = 'conjoins_any_of_and_comparison_predicates'
|
||||
Command = 'cargo test --test selection'
|
||||
File = 'src/selection.rs'
|
||||
Old = 'if !values.iter().any(|v| wanted.iter().any(|w| w == v)) {'
|
||||
New = 'if false {'
|
||||
},
|
||||
@{
|
||||
Id = 'RS-S6-Revert'
|
||||
Rule = 'S6: cites.role, when present, filters which declared reference the hop follows.'
|
||||
Test = 'cites_role_excludes_a_second_reference_at_the_same_target'
|
||||
Command = 'cargo test --test selection'
|
||||
File = 'src/selection.rs'
|
||||
Old = "if let Some(wanted_role) = &citation.role
|
||||
&& *wanted_role != role
|
||||
{"
|
||||
New = 'if false {'
|
||||
},
|
||||
@{
|
||||
Id = 'RS-EmptySchemas-Revert'
|
||||
Rule = 'S1: an empty (but present) schemas list is refused, never answered as an empty page.'
|
||||
|
|
@ -225,9 +245,16 @@
|
|||
Id = 'RS-V0Lowering-Revert'
|
||||
Rule = 'v0''s own cleaning: an empty or all-blank v0 list lowers to null (no filter), not to a list the door or the evaluator would have to refuse or misread.'
|
||||
Test = 'serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter'
|
||||
Command = 'cargo test --test cultnet'
|
||||
# Narrowed to this test alone, not the whole tests/cultnet.rs binary: that binary carries
|
||||
# reactive_document_coalesces_direct_same_schema_alias_member_writes, a pre-existing test
|
||||
# unrelated to this campaign that fails under a full-binary run (both parallel and
|
||||
# RUST_TEST_THREADS=1) but passes every time in isolation - state pollution from another
|
||||
# test in that binary, not a correctness question this entry is about. Filtering M0 and
|
||||
# the entry run to this test's own name keeps this suite from depending on that binary's
|
||||
# unrelated health; the flake itself is a separate finding, not fixed here.
|
||||
Command = 'cargo test --test cultnet serve_read_only_raw_snapshot'
|
||||
File = 'src/snapshot_query.rs'
|
||||
Old = 'Some(values) if !values.is_empty() && values.iter().any(|value| !value.trim().is_empty()) => {'
|
||||
New = 'Some(values) => {'
|
||||
Old = 'if filtered.is_empty() { None } else { Some(filtered) }'
|
||||
New = 'Some(filtered)'
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ namespace GameCult.Networking.Tests
|
|||
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[1]");
|
||||
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}";
|
||||
|
|
|
|||
Loading…
Reference in a new issue