lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

rshr_202_step_299_gate.rs (22306B)


      1 use std::env;
      2 use std::fs;
      3 use std::path::{Path, PathBuf};
      4 use std::process::{Command, Output};
      5 
      6 use serde_json::{Value, json};
      7 use sha2::{Digest, Sha256};
      8 
      9 const STEP: u16 = 299;
     10 const GATE_DIGEST: &str = "b76f61a9a5365ca088d4b00eb140ed30b0b51f418cba68e26e3f3cf1c0ff3a15";
     11 const NIX_SHA256: &str = "a59ab70f97f6d571642d13c7506aafec0a4275520d53daee2d8451be7c495cd1";
     12 const NIX_VERSION_SHA256: &str = "6db806391ffaea4cdb08ade0031feac399c0cd08474b3bfde8cb33f88a36c8e1";
     13 const MAX_OUTPUT_BYTES: usize = 32 * 1024 * 1024;
     14 
     15 const EXACT_SOURCES: &[(&str, &str)] = &[
     16     (
     17         "flake.nix",
     18         "de3d2258f2c48ae8b1493b676ab3d1466cd5f1a59015f65283efbfe12447e501",
     19     ),
     20     (
     21         "build/nix/library.nix",
     22         "ff425fe6361bad78c105e36d3e900950ce22b0034e26add55e5ed287aa868765",
     23     ),
     24     (
     25         "build/nix/service/native-inputs.nix",
     26         "09aed688d6ad6c5c824aa771b871cc0c4857d8378d1330221c8d5a05509d9391",
     27     ),
     28     (
     29         "build/nix/service/nixos-module.nix",
     30         "20b42dd6972c59fcaaf5a282219644d83822ea03a2805bfe8e6e9b8f7b628622",
     31     ),
     32     (
     33         "build/nix/service/fixture.nix",
     34         "f3fa0cbef4f6a86feee9b2319165d97c51526236b1426ffa83ffb435ed90cd97",
     35     ),
     36     (
     37         "build/nix/service/systems.nix",
     38         "d16e21827022a2315234f4c5e4b485017a36ecd90a5559b01d23331cdd505e46",
     39     ),
     40 ];
     41 
     42 pub(crate) struct Arguments {
     43     pub(crate) step: u16,
     44     pub(crate) check_id: String,
     45     pub(crate) source_revision: String,
     46     pub(crate) source_tree: String,
     47     pub(crate) candidate_digest: String,
     48     pub(crate) platform: String,
     49     pub(crate) execution_request_sha256: String,
     50 }
     51 
     52 fn root() -> PathBuf {
     53     Path::new(env!("CARGO_MANIFEST_DIR"))
     54         .parent()
     55         .and_then(Path::parent)
     56         .expect("xtask must remain under tools/xtask")
     57         .to_path_buf()
     58 }
     59 
     60 fn sha256(bytes: &[u8]) -> String {
     61     hex::encode(Sha256::digest(bytes))
     62 }
     63 
     64 fn canonical(value: &Value) -> Result<Vec<u8>, String> {
     65     serde_json::to_vec(value).map_err(|_| "Step 299 JSON encoding failed".to_owned())
     66 }
     67 
     68 fn execute(command: &mut Command, label: &str) -> Result<Output, String> {
     69     let output = command
     70         .current_dir(root())
     71         .env("CARGO_NET_OFFLINE", "true")
     72         .env("CARGO_TERM_COLOR", "never")
     73         .output()
     74         .map_err(|_| format!("{label} could not start"))?;
     75     if output.stdout.len() > MAX_OUTPUT_BYTES || output.stderr.len() > MAX_OUTPUT_BYTES {
     76         return Err(format!("{label} exceeded its output bound"));
     77     }
     78     Ok(output)
     79 }
     80 
     81 fn bounded(command: &mut Command, label: &str) -> Result<Output, String> {
     82     let output = execute(command, label)?;
     83     if !output.status.success() {
     84         return Err(format!("{label} failed"));
     85     }
     86     Ok(output)
     87 }
     88 
     89 fn rejected(command: &mut Command, label: &str) -> Result<(), String> {
     90     if execute(command, label)?.status.success() {
     91         return Err(format!("{label} unexpectedly succeeded"));
     92     }
     93     Ok(())
     94 }
     95 
     96 fn resolve_nix() -> Result<PathBuf, String> {
     97     if let Some(explicit) = env::var_os("RSHR_NIX_EXECUTABLE") {
     98         return fs::canonicalize(explicit)
     99             .map_err(|_| "Step 299 Nix client is unavailable".to_owned());
    100     }
    101     let path = env::var_os("PATH").ok_or_else(|| "Step 299 PATH is absent".to_owned())?;
    102     env::split_paths(&path)
    103         .map(|directory| directory.join("nix"))
    104         .find(|candidate| candidate.is_file())
    105         .and_then(|candidate| fs::canonicalize(candidate).ok())
    106         .ok_or_else(|| "Step 299 Nix client is unavailable".to_owned())
    107 }
    108 
    109 fn object_keys(value: &Value, label: &str) -> Result<Vec<String>, String> {
    110     let mut keys = value
    111         .as_object()
    112         .ok_or_else(|| format!("Step 299 {label} is not an object"))?
    113         .keys()
    114         .cloned()
    115         .collect::<Vec<_>>();
    116     keys.sort_unstable();
    117     Ok(keys)
    118 }
    119 
    120 fn require_outputs(nix: &Path) -> Result<(), String> {
    121     let show = bounded(
    122         Command::new(nix).args([
    123             "--offline",
    124             "flake",
    125             "show",
    126             "--json",
    127             "--all-systems",
    128             "--no-write-lock-file",
    129         ]),
    130         "Step 299 Nix output inventory",
    131     )?;
    132     let inventory: Value = serde_json::from_slice(&show.stdout)
    133         .map_err(|_| "Step 299 Nix output inventory is invalid".to_owned())?;
    134     validate_output_inventory(&inventory)?;
    135 
    136     let supported = bounded(
    137         Command::new(nix).args(["--offline", "eval", "--json", ".#lib.supportedSystems"]),
    138         "Step 299 shared-helper systems",
    139     )?;
    140     if supported.stdout != b"[\"aarch64-darwin\",\"x86_64-linux\"]\n" {
    141         return Err("Step 299 shared-helper systems differ".to_owned());
    142     }
    143     let helper_type = bounded(
    144         Command::new(nix).args([
    145             "--offline",
    146             "eval",
    147             "--raw",
    148             "--apply",
    149             "f: builtins.typeOf f",
    150             ".#lib.mkServiceHelpers",
    151         ]),
    152         "Step 299 shared-helper export",
    153     )?;
    154     if helper_type.stdout != b"lambda" {
    155         return Err("Step 299 shared-helper export differs".to_owned());
    156     }
    157 
    158     for system in ["x86_64-darwin", "aarch64-linux", "x86_64-windows"] {
    159         rejected(
    160             Command::new(nix).args([
    161                 "--offline",
    162                 "eval",
    163                 "--raw",
    164                 &format!(".#packages.{system}.default.name"),
    165             ]),
    166             "Step 299 excluded-system evaluation",
    167         )?;
    168     }
    169     Ok(())
    170 }
    171 
    172 fn validate_output_inventory(inventory: &Value) -> Result<(), String> {
    173     let systems = ["aarch64-darwin", "x86_64-linux"];
    174     for family in ["apps", "checks", "devShells", "formatter", "packages"] {
    175         if object_keys(&inventory[family], family)? != systems {
    176             return Err(format!("Step 299 {family} systems differ"));
    177         }
    178     }
    179     if inventory.pointer("/overlays/default/type") != Some(&json!("nixpkgs-overlay")) {
    180         return Err("Step 299 default overlay is absent".to_owned());
    181     }
    182     for system in systems {
    183         let packages = &inventory["packages"][system];
    184         if object_keys(packages, "packages")? != ["default", "xtask"]
    185             || packages["default"]["name"] != "radroots-lib-release-bundle-0.1.0-alpha"
    186         {
    187             return Err("Step 299 package inventory differs".to_owned());
    188         }
    189         if inventory["apps"][system]["default"]["description"]
    190             != "Inspect the installed Radroots Lib release bundle"
    191             || inventory["devShells"][system].get("default").is_none()
    192             || inventory["checks"][system].get("release-bundle").is_none()
    193         {
    194             return Err("Step 299 production output is absent".to_owned());
    195         }
    196         for family in ["apps", "devShells", "packages"] {
    197             if object_keys(&inventory[family][system], family)?
    198                 .iter()
    199                 .any(|name| name.contains("fixture"))
    200             {
    201                 return Err("Step 299 fixture escaped its test-only surface".to_owned());
    202             }
    203         }
    204         let fixture_checks = object_keys(&inventory["checks"][system], "checks")?
    205             .into_iter()
    206             .filter(|name| name.contains("fixture"))
    207             .collect::<Vec<_>>();
    208         if fixture_checks.is_empty()
    209             || fixture_checks
    210                 .iter()
    211                 .any(|name| !name.starts_with("service-fixture-"))
    212         {
    213             return Err("Step 299 fixture check names differ".to_owned());
    214         }
    215     }
    216 
    217     Ok(())
    218 }
    219 
    220 fn require_nix() -> Result<(), String> {
    221     let executable = resolve_nix()?;
    222     if sha256(&fs::read(&executable).map_err(|_| "Step 299 Nix client is unreadable")?)
    223         != NIX_SHA256
    224     {
    225         return Err("Step 299 Nix client identity differs".to_owned());
    226     }
    227     let version = bounded(
    228         Command::new(&executable).arg("--version"),
    229         "Step 299 Nix version",
    230     )?;
    231     if sha256(&version.stdout) != NIX_VERSION_SHA256 {
    232         return Err("Step 299 Nix version differs".to_owned());
    233     }
    234     bounded(
    235         Command::new(&executable).args([
    236             "--offline",
    237             "flake",
    238             "check",
    239             "--all-systems",
    240             "--no-build",
    241             "--no-write-lock-file",
    242         ]),
    243         "Step 299 Nix evaluation",
    244     )?;
    245     require_outputs(&executable)
    246 }
    247 
    248 fn expected_contract(verifier_sha256: &str) -> Value {
    249     json!({
    250         "argv_template": [
    251             "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked",
    252             "-q", "-p", "xtask", "--", "rshr-step-299-gate", "--step={step}",
    253             "--check-id={check_id}", "--source-revision={source_revision}",
    254             "--source-tree={source_tree}", "--candidate-digest={candidate_digest}",
    255             "--platform=macos_aarch64",
    256             "--execution-request-sha256={execution_request_sha256}"
    257         ],
    258         "assertion_id": [format!("step_299_gate_01_{GATE_DIGEST}")],
    259         "check_id": format!("gate-01-{GATE_DIGEST}"),
    260         "environment_authority": {
    261             "cache_policy_id": "rshr-200-step-287-cache-policy.v1",
    262             "cache_policy_sha256": "3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa",
    263             "cadence_policy_id": "rshr-200-step-287-cadence-policy.v1",
    264             "cadence_policy_sha256": "d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1",
    265             "isolation": "extbuild_host_constrained",
    266             "network": "disabled",
    267             "network_policy_id": "none",
    268             "network_policy_sha256": "none",
    269             "resource_policy_id": "rshr-200-step-287-resource-policy.v1",
    270             "resource_policy_sha256": "05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e"
    271         },
    272         "environment_names": [
    273             "EXT_BUILD_CONFIG", "EXT_BUILD_MACHINE_CONFIG", "EXT_BUILD_ROOT", "HOME", "PATH",
    274             "RUSTUP_TOOLCHAIN", "TMPDIR"
    275         ],
    276         "gate_definition_sha256": GATE_DIGEST,
    277         "required_platforms": ["macos_aarch64"],
    278         "required_tools": ["rustc"],
    279         "result_schema": "radroots.services-hardening.rshr-200-step-check-result.v1",
    280         "schema": "radroots.services-hardening.rshr-200-step-check-command.v1",
    281         "step": STEP,
    282         "verifier_path": "tools/xtask/src/rshr_202_step_299_gate.rs",
    283         "verifier_sha256": verifier_sha256
    284     })
    285 }
    286 
    287 fn validate_arguments(arguments: &Arguments) -> Result<String, String> {
    288     let check_id = format!("gate-01-{GATE_DIGEST}");
    289     if arguments.step != STEP
    290         || arguments.check_id != check_id
    291         || arguments.candidate_digest != "none"
    292         || arguments.platform != "macos_aarch64"
    293         || arguments.source_revision.len() != 40
    294         || arguments.source_tree.len() != 40
    295         || arguments.execution_request_sha256.len() != 64
    296         || !arguments
    297             .source_revision
    298             .bytes()
    299             .chain(arguments.source_tree.bytes())
    300             .chain(arguments.execution_request_sha256.bytes())
    301             .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
    302     {
    303         return Err("Step 299 gate arguments differ".to_owned());
    304     }
    305     Ok(check_id)
    306 }
    307 
    308 pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
    309     let check_id = validate_arguments(&arguments)?;
    310     let root = root();
    311     if root.join(".github").exists() {
    312         return Err("forbidden .github surface is present".to_owned());
    313     }
    314     for (relative, expected) in EXACT_SOURCES {
    315         let bytes = fs::read(root.join(relative))
    316             .map_err(|_| "Step 299 governed source is unreadable".to_owned())?;
    317         if sha256(&bytes) != *expected {
    318             return Err("Step 299 governed source bytes differ".to_owned());
    319         }
    320     }
    321 
    322     let verifier_path = root.join("tools/xtask/src/rshr_202_step_299_gate.rs");
    323     let verifier_sha256 =
    324         sha256(&fs::read(verifier_path).map_err(|_| "Step 299 verifier is unreadable".to_owned())?);
    325     let authority_path = root.join("contracts/rshr-202-step-299-gates.v1.json");
    326     let authority_bytes =
    327         fs::read(authority_path).map_err(|_| "Step 299 gate authority is unreadable".to_owned())?;
    328     let contract = validate_authority(&authority_bytes, &verifier_sha256)?;
    329 
    330     bounded(
    331         Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]),
    332         "Step 299 formatting",
    333     )?;
    334     bounded(
    335         Command::new("cargo").args(["+1.97.1", "check", "--offline", "--locked", "-p", "xtask"]),
    336         "Step 299 verifier check",
    337     )?;
    338     require_nix()?;
    339 
    340     let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?;
    341     std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
    342         .map_err(|_| "Step 299 result write failed".to_owned())
    343 }
    344 
    345 fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> {
    346     let authority: Value = serde_json::from_slice(authority_bytes)
    347         .map_err(|_| "Step 299 gate authority is invalid".to_owned())?;
    348     let mut canonical_authority = canonical(&authority)?;
    349     canonical_authority.push(b'\n');
    350     let contracts = authority
    351         .get("gate_command_contract")
    352         .and_then(Value::as_array)
    353         .ok_or_else(|| "Step 299 gate contract is absent".to_owned())?;
    354     if authority_bytes != canonical_authority
    355         || authority.get("schema")
    356             != Some(&Value::String(
    357                 "radroots.lib.rshr-202-step-299-gates.v1".to_owned(),
    358             ))
    359         || authority.get("step") != Some(&json!([STEP]))
    360         || contracts.as_slice() != [expected_contract(verifier_sha256)]
    361     {
    362         return Err("Step 299 gate authority differs".to_owned());
    363     }
    364     Ok(contracts[0].clone())
    365 }
    366 
    367 fn result_bytes(
    368     arguments: &Arguments,
    369     check_id: &str,
    370     verifier_sha256: &str,
    371     contract: &Value,
    372 ) -> Result<Vec<u8>, String> {
    373     let assertion = json!([{
    374         "id": format!("step_299_gate_01_{GATE_DIGEST}"),
    375         "result": "pass"
    376     }]);
    377     let result = json!({
    378         "schema": "radroots.services-hardening.rshr-200-step-check-result.v1",
    379         "step": STEP,
    380         "check_id": check_id,
    381         "gate_definition_sha256": GATE_DIGEST,
    382         "source_revision": arguments.source_revision,
    383         "source_tree": arguments.source_tree,
    384         "candidate_generation": 0,
    385         "candidate_digest": "none",
    386         "command_contract_sha256": sha256(&canonical(contract)?),
    387         "verifier_sha256": verifier_sha256,
    388         "execution_request": [{
    389             "platform": arguments.platform,
    390             "sha256": arguments.execution_request_sha256
    391         }],
    392         "assertion_inventory_sha256": sha256(&canonical(&assertion)?),
    393         "assertion": assertion,
    394         "result": "pass"
    395     });
    396     let mut bytes = canonical(&result)?;
    397     bytes.push(b'\n');
    398     Ok(bytes)
    399 }
    400 
    401 #[cfg(test)]
    402 mod tests {
    403     use super::*;
    404 
    405     #[test]
    406     fn command_capture_preserves_status_streams_and_enforces_both_output_limits() {
    407         let output = bounded(
    408             Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]),
    409             "fixture",
    410         )
    411         .unwrap();
    412         assert_eq!(output.stdout, b"output");
    413         assert_eq!(output.stderr, b"diagnostic");
    414         assert_eq!(
    415             bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(),
    416             "fixture failed"
    417         );
    418         let missing = tempfile::TempDir::new().unwrap();
    419         assert_eq!(
    420             bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(),
    421             "fixture could not start"
    422         );
    423         for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] {
    424             let maximum = MAX_OUTPUT_BYTES.to_string();
    425             let output = bounded(
    426                 Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]),
    427                 "fixture",
    428             )
    429             .unwrap();
    430             assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES);
    431             let oversized = (MAX_OUTPUT_BYTES + 1).to_string();
    432             assert_eq!(
    433                 bounded(
    434                     Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]),
    435                     "fixture"
    436                 )
    437                 .unwrap_err(),
    438                 "fixture exceeded its output bound"
    439             );
    440         }
    441     }
    442 
    443     #[test]
    444     fn expected_command_rejection_requires_a_nonzero_exit() {
    445         rejected(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap();
    446         assert_eq!(
    447             rejected(Command::new("/bin/sh").args(["-c", "exit 0"]), "fixture").unwrap_err(),
    448             "fixture unexpectedly succeeded"
    449         );
    450     }
    451 
    452     fn arguments() -> Arguments {
    453         Arguments {
    454             step: STEP,
    455             check_id: format!("gate-01-{GATE_DIGEST}"),
    456             source_revision: "a".repeat(40),
    457             source_tree: "0".repeat(40),
    458             candidate_digest: "none".into(),
    459             platform: "macos_aarch64".into(),
    460             execution_request_sha256: "1".repeat(64),
    461         }
    462     }
    463 
    464     #[test]
    465     fn invalid_gate_arguments_are_rejected_before_external_work() {
    466         assert_eq!(
    467             validate_arguments(&arguments()).unwrap(),
    468             format!("gate-01-{GATE_DIGEST}")
    469         );
    470         for field in 0..8 {
    471             let mut invalid = arguments();
    472             match field {
    473                 0 => invalid.step = 0,
    474                 1 => invalid.check_id.clear(),
    475                 2 => invalid.candidate_digest = "unbound".into(),
    476                 3 => invalid.platform = "linux".into(),
    477                 4 => invalid.source_revision.clear(),
    478                 5 => invalid.source_tree.clear(),
    479                 6 => invalid.execution_request_sha256.clear(),
    480                 _ => invalid.source_revision = "A".repeat(40),
    481             }
    482             assert_eq!(run(invalid).unwrap_err(), "Step 299 gate arguments differ");
    483         }
    484         let mut invalid = arguments();
    485         invalid.source_tree = "g".repeat(40);
    486         assert!(validate_arguments(&invalid).is_err());
    487     }
    488 
    489     #[test]
    490     fn authority_requires_canonical_bytes_and_exact_retained_bindings() {
    491         let raw = include_bytes!("../../../contracts/rshr-202-step-299-gates.v1.json");
    492         let authority: Value = serde_json::from_slice(raw).unwrap();
    493         let verifier = authority["gate_command_contract"][0]["verifier_sha256"]
    494             .as_str()
    495             .unwrap();
    496         let contract = validate_authority(raw, verifier).unwrap();
    497         assert!(validate_authority(raw, &"f".repeat(64)).is_err());
    498         assert!(validate_authority(b"invalid", verifier).is_err());
    499         assert!(validate_authority(b"{}\n", verifier).is_err());
    500         assert!(
    501             validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err()
    502         );
    503         for (pointer, value) in [
    504             ("/schema", json!("other")),
    505             ("/step", json!([0])),
    506             ("/gate_command_contract", json!([])),
    507         ] {
    508             let mut changed = authority.clone();
    509             *changed.pointer_mut(pointer).unwrap() = value;
    510             let mut bytes = canonical(&changed).unwrap();
    511             bytes.push(b'\n');
    512             assert!(validate_authority(&bytes, verifier).is_err());
    513         }
    514         // Encoding fixtures is not a historical gate execution or qualification.
    515         let args = arguments();
    516         let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap();
    517         let result: Value = serde_json::from_slice(&bytes).unwrap();
    518         assert!(bytes.ends_with(b"\n"));
    519         assert_eq!(result["source_revision"], args.source_revision);
    520         assert_eq!(result["source_tree"], args.source_tree);
    521         assert_eq!(
    522             result["execution_request"][0]["sha256"],
    523             args.execution_request_sha256
    524         );
    525         assert_eq!(
    526             result["command_contract_sha256"],
    527             sha256(&canonical(&contract).unwrap())
    528         );
    529         assert_eq!(
    530             result["assertion_inventory_sha256"],
    531             sha256(&canonical(&result["assertion"]).unwrap())
    532         );
    533     }
    534 
    535     #[test]
    536     fn retained_inventory_rejects_platform_drift_and_fixture_escape_without_nix() {
    537         let mut inventory = json!({"overlays":{"default":{"type":"nixpkgs-overlay"}}});
    538         for system in ["aarch64-darwin", "x86_64-linux"] {
    539             inventory["packages"][system] =
    540                 json!({"default":{"name":"radroots-lib-release-bundle-0.1.0-alpha"},"xtask":{}});
    541             inventory["apps"][system] = json!({"default":{"description":"Inspect the installed Radroots Lib release bundle"}});
    542             inventory["devShells"][system] = json!({"default":{}});
    543             inventory["checks"][system] =
    544                 json!({"release-bundle":{}, "service-fixture-example":{}});
    545             inventory["formatter"][system] = json!({});
    546         }
    547         validate_output_inventory(&inventory).unwrap();
    548         for family in ["apps", "checks", "devShells", "formatter", "packages"] {
    549             let mut changed = inventory.clone();
    550             changed[family]
    551                 .as_object_mut()
    552                 .unwrap()
    553                 .remove("aarch64-darwin");
    554             assert!(validate_output_inventory(&changed).is_err());
    555         }
    556         assert!(object_keys(&Value::Null, "test").is_err());
    557         for (pointer, value) in [
    558             ("/overlays/default/type", json!("wrong")),
    559             ("/packages/aarch64-darwin/default/name", json!("wrong")),
    560             ("/apps/aarch64-darwin/default/description", json!("wrong")),
    561             ("/devShells/aarch64-darwin", json!({})),
    562             ("/checks/aarch64-darwin", json!({})),
    563             ("/checks/aarch64-darwin", json!({"release-bundle":{}})),
    564             (
    565                 "/checks/aarch64-darwin",
    566                 json!({"release-bundle":{},"bad-fixture":{}}),
    567             ),
    568         ] {
    569             let mut changed = inventory.clone();
    570             *changed.pointer_mut(pointer).unwrap() = value;
    571             assert!(validate_output_inventory(&changed).is_err());
    572         }
    573         for family in ["apps", "devShells", "packages"] {
    574             let mut changed = inventory.clone();
    575             changed[family]["aarch64-darwin"]["fixture-escape"] = json!({});
    576             assert!(validate_output_inventory(&changed).is_err());
    577         }
    578     }
    579 }