Merge interop Cut 0: the interop lane runs, and fails honestly
The consolidation guard accepts cultnet-rs vendored schema copies (the lane
was red for 40 main runs). Every CI step now fails on any failed command
(job default shell bash -eo pipefail); proven red on a throwaway branch.
The cultnet-py explicit-bind test stays on loopback; the Rust drift guard
lists the vendored directory. CI run 36723314611 green at 59d85557.
Follow-up: CI never runs schema_discovery_contracts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
36ea08d38d
3 changed files with 65 additions and 66 deletions
13
.github/workflows/cultnet-interop.yml
vendored
13
.github/workflows/cultnet-interop.yml
vendored
|
|
@ -12,6 +12,13 @@ jobs:
|
|||
name: C# / TypeScript / Rust / Python / Kotlin
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
# Windows runners default to pwsh, which does not stop when a native command fails: a
|
||||
# multi-command step reported only its last command, and a failing cultnet-py suite
|
||||
# went green that way. Git Bash runs as `bash -e -o pipefail`, so every command counts.
|
||||
# Steps that name `shell: pwsh` check $LASTEXITCODE themselves.
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
DOTNET: dotnet
|
||||
|
|
@ -69,7 +76,11 @@ jobs:
|
|||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$stray = git ls-files '*.schema.json' | Where-Object { $_ -notmatch '^contracts/cultnet/' }
|
||||
# packages/cultnet-rs/contracts/cultnet/ holds R-AB's vendored byte copies (the crate must build
|
||||
# when copied alone). Their drift guard is packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99.
|
||||
$tracked = git ls-files '*.schema.json'
|
||||
if ($LASTEXITCODE -ne 0) { throw "git ls-files failed" }
|
||||
$stray = $tracked | Where-Object { $_ -notmatch '^(packages/cultnet-rs/)?contracts/cultnet/' }
|
||||
# -I skips binary files: unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll
|
||||
# still embeds the retired namespace until it is rebuilt (REC-032 D4, deferred).
|
||||
$oldNamespace = git grep -I -lE 'GameCult/cultnet-ts/(integration/)?contracts' -- ':!.voidbot'
|
||||
|
|
|
|||
|
|
@ -2603,24 +2603,31 @@ class CultNetRudpClientBindTests(unittest.TestCase):
|
|||
finally:
|
||||
listener.close()
|
||||
|
||||
# The endpoint alone would bind 127.0.0.1; the explicit host must win, and the
|
||||
# source address shows which one was bound. It stays on loopback, because a
|
||||
# loopback-bound socket cannot send off the host on Windows.
|
||||
def test_a_schema_transport_binds_an_explicit_bind_host(self) -> None:
|
||||
address = _non_loopback_ipv4_address()
|
||||
if address is None:
|
||||
self.skipTest("no non-loopback IPv4 address on this host")
|
||||
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
probe.bind(("127.0.0.2", 0))
|
||||
except OSError:
|
||||
self.skipTest("127.0.0.2 is not a local address on this host")
|
||||
finally:
|
||||
probe.close()
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
listener.bind(("0.0.0.0", 0))
|
||||
listener.bind(("127.0.0.1", 0))
|
||||
listener.settimeout(2.0)
|
||||
try:
|
||||
with self.assertRaises(TimeoutError):
|
||||
create_rudp_schema_transport(
|
||||
host=address,
|
||||
host="127.0.0.1",
|
||||
port=listener.getsockname()[1],
|
||||
connection_id=0x43554C54,
|
||||
timeout_seconds=0.2,
|
||||
bind_host="127.0.0.1",
|
||||
bind_host="127.0.0.2",
|
||||
)
|
||||
_, source = listener.recvfrom(65535)
|
||||
self.assertEqual(source[0], "127.0.0.1")
|
||||
self.assertEqual(source[0], "127.0.0.2")
|
||||
finally:
|
||||
listener.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,85 +18,66 @@
|
|||
//! Vendoring is a second copy of files this repo already treats as canonical
|
||||
//! (`contracts/cultnet/*.schema.json`, the shared source multiple runtimes compile against per
|
||||
//! `docs/cultnet-selection-cut.md` section 1). A second copy can drift silently, so this test is
|
||||
//! the thing that makes that impossible without a loud, immediate failure: every vendored file is
|
||||
//! compared byte-for-byte against its repo-root original. It skips (not fails) when the canonical
|
||||
//! the thing that makes that impossible without a loud, immediate failure: every `*.schema.json`
|
||||
//! under the vendored `contracts/cultnet/` is compared byte-for-byte against its repo-root
|
||||
//! original. The files are found by listing the directory, not from a list kept by hand, so a newly
|
||||
//! vendored copy cannot escape the check. It skips (not fails) when the canonical
|
||||
//! `contracts/` tree is not present beside the package - i.e. exactly the isolated-copy situation
|
||||
//! R-AB exists to survive - so this check does not reintroduce the same escape it is guarding
|
||||
//! against.
|
||||
//! against. A schema embedded by `schema_discovery.rs` but not vendored fails to compile.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Every schema this crate's `schema_discovery::builtin_schema_registry` embeds via
|
||||
/// `include_str!("../contracts/cultnet/…")` - kept in sync with `schema_discovery.rs` by hand;
|
||||
/// a schema added there and not here is still caught, because the crate would then fail to
|
||||
/// compile against a file this list never copied to `contracts/cultnet/`.
|
||||
const VENDORED_SCHEMAS: &[&str] = &[
|
||||
"cultnet.hello.schema.json",
|
||||
"cultnet.document-mutation-contract.schema.json",
|
||||
"cultnet.transport-profile.schema.json",
|
||||
"cultnet.login.schema.json",
|
||||
"cultnet.register.schema.json",
|
||||
"cultnet.verify.schema.json",
|
||||
"cultnet.login-success.schema.json",
|
||||
"cultnet.error.schema.json",
|
||||
"cultnet.sample-change-name.schema.json",
|
||||
"cultnet.sample-chat.schema.json",
|
||||
"cultnet.document-put.schema.json",
|
||||
"cultnet.document-delete.schema.json",
|
||||
"cultnet.raw-document-record.schema.json",
|
||||
"cultnet.document-put-raw.schema.json",
|
||||
"cultnet.snapshot-request.schema.json",
|
||||
"cultnet.snapshot-response.schema.json",
|
||||
"cultnet.snapshot-response-raw.schema.json",
|
||||
"cultnet.schema-catalog-request.schema.json",
|
||||
"cultnet.schema-catalog-response.schema.json",
|
||||
"cultnet.shard-catalog-request.schema.json",
|
||||
"cultnet.shard-catalog-response.schema.json",
|
||||
"ghostlight.agent-state.schema.json",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn vendored_contract_schemas_are_present_and_match_every_include_str_call() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let vendored_root = manifest_dir.join("contracts/cultnet");
|
||||
for name in VENDORED_SCHEMAS {
|
||||
let path = vendored_root.join(name);
|
||||
assert!(
|
||||
path.is_file(),
|
||||
"{name} is embedded by schema_discovery.rs but missing from the vendored \
|
||||
contracts/cultnet/ copy at {path:?}"
|
||||
);
|
||||
/// Every `*.schema.json` under `root`, as paths relative to it, sorted.
|
||||
fn schema_files(root: &Path) -> Vec<PathBuf> {
|
||||
fn walk(root: &Path, dir: &Path, found: &mut Vec<PathBuf>) {
|
||||
for entry in fs::read_dir(dir).unwrap_or_else(|error| panic!("listing {dir:?}: {error}")) {
|
||||
let path = entry.expect("directory entry").path();
|
||||
if path.is_dir() {
|
||||
walk(root, &path, found);
|
||||
} else if path.to_string_lossy().ends_with(".schema.json") {
|
||||
found.push(path.strip_prefix(root).expect("path under root").to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut found = Vec::new();
|
||||
walk(root, root, &mut found);
|
||||
found.sort();
|
||||
found
|
||||
}
|
||||
|
||||
fn vendored_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("contracts/cultnet")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vendored_contract_schemas_match_the_canonical_repo_root_source() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let vendored_root = manifest_dir.join("contracts/cultnet");
|
||||
// packages/cultnet-rs -> packages -> repo root -> contracts/cultnet
|
||||
let canonical_root = manifest_dir.join("../../contracts/cultnet");
|
||||
let canonical_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts/cultnet");
|
||||
|
||||
if !canonical_root.is_dir() {
|
||||
// R-AB: exactly the isolated-copy case this crate's own vendored files exist to survive
|
||||
// (cargo-mutants, or any tool that copies only this package's directory). Nothing to
|
||||
// compare against, and that is expected, not a failure.
|
||||
eprintln!(
|
||||
"skipping: {canonical_root:?} not present beside the package (isolated build, \
|
||||
the case R-AB's vendored copies are for)"
|
||||
"skipping: {canonical_root:?} not present beside the package (isolated build, the case R-AB's vendored copies are for)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for name in VENDORED_SCHEMAS {
|
||||
let canonical = fs::read_to_string(canonical_root.join(name))
|
||||
.unwrap_or_else(|error| panic!("reading canonical contracts/cultnet/{name}: {error}"));
|
||||
let vendored = fs::read_to_string(vendored_root.join(name))
|
||||
.unwrap_or_else(|error| panic!("reading vendored contracts/cultnet/{name}: {error}"));
|
||||
let vendored = schema_files(&vendored_root());
|
||||
assert!(!vendored.is_empty(), "no vendored schemas found under {:?}", vendored_root());
|
||||
for name in vendored {
|
||||
let shown = name.display();
|
||||
let canonical = fs::read_to_string(canonical_root.join(&name)).unwrap_or_else(|error| {
|
||||
panic!("packages/cultnet-rs/contracts/cultnet/{shown} has no canonical contracts/cultnet/{shown}: {error}")
|
||||
});
|
||||
let copy = fs::read_to_string(vendored_root().join(&name))
|
||||
.unwrap_or_else(|error| panic!("reading vendored contracts/cultnet/{shown}: {error}"));
|
||||
assert_eq!(
|
||||
canonical, vendored,
|
||||
"packages/cultnet-rs/contracts/cultnet/{name} has drifted from the canonical \
|
||||
contracts/cultnet/{name} - re-copy it"
|
||||
canonical, copy,
|
||||
"packages/cultnet-rs/contracts/cultnet/{shown} has drifted from the canonical contracts/cultnet/{shown} - re-copy it"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +88,7 @@ fn builtin_schema_registry_loads_every_vendored_schema() {
|
|||
let listed = registry.list(&cultnet_rs::CultNetSchemaCatalogOptions::default());
|
||||
assert_eq!(
|
||||
listed.len(),
|
||||
VENDORED_SCHEMAS.len(),
|
||||
schema_files(&vendored_root()).len(),
|
||||
"one registration per vendored schema file"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue