diff --git a/packages/cultnet-rs/tests/schema_discovery_contracts.rs b/packages/cultnet-rs/tests/schema_discovery_contracts.rs index 7c62b80f..833f40c3 100644 --- a/packages/cultnet-rs/tests/schema_discovery_contracts.rs +++ b/packages/cultnet-rs/tests/schema_discovery_contracts.rs @@ -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 { + fn walk(root: &Path, dir: &Path, found: &mut Vec) { + 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" ); }