#![allow(clippy::too_many_lines, clippy::type_complexity)]
use jsonschema::Validator;
use scrollcase_consumer::release::{BoxManifest, ReleaseManifest};
use serde_json::{json, Value};
const RELEASE_SCHEMA: &str = include_str!("../src/contract/schema/release-manifest.schema.json");
const BOX_SCHEMA: &str = include_str!("../src/contract/schema/box-manifest.schema.json");
const TARGET_SCHEMA: &str = include_str!("../src/contract/schema/target.schema.json");
const EXECUTION_SCHEMA: &str = include_str!("../src/contract/schema/execution.schema.json");
const RELEASE_EXAMPLE: &str = include_str!("../fixtures/examples/release-manifest.example.json");
const BOX_EXAMPLE: &str = include_str!("../fixtures/examples/box-manifest.example.json");
fn registry() -> jsonschema::Registry<'static> {
let mut builder = jsonschema::Registry::new();
for raw in [RELEASE_SCHEMA, BOX_SCHEMA, TARGET_SCHEMA, EXECUTION_SCHEMA] {
let schema: Value = serde_json::from_str(raw).unwrap();
let id = schema["$id"].as_str().unwrap().to_string();
builder = builder
.add(id, jsonschema::Resource::from_contents(schema))
.expect("canonical schema must register");
}
builder.prepare().expect("registry must prepare")
}
fn validator(registry: &jsonschema::Registry, root: &str) -> Validator {
jsonschema::options()
.with_registry(registry)
.build(&serde_json::from_str::<Value>(root).unwrap())
.expect("canonical schema must compile")
}
fn types_accept_release(value: &Value) -> bool {
serde_json::from_value::<ReleaseManifest>(value.clone())
.is_ok_and(|release| release.validate().is_ok())
}
#[test]
fn the_canonical_examples_pass_both_the_schema_and_the_types() {
let registry = registry();
let release: Value = serde_json::from_str(RELEASE_EXAMPLE).unwrap();
assert!(validator(®istry, RELEASE_SCHEMA).is_valid(&release));
assert!(types_accept_release(&release));
let box_manifest: Value = serde_json::from_str(BOX_EXAMPLE).unwrap();
assert!(validator(®istry, BOX_SCHEMA).is_valid(&box_manifest));
assert!(serde_json::from_value::<BoxManifest>(box_manifest).is_ok());
}
#[test]
fn the_types_and_the_schema_agree_on_every_mutation() {
let registry = registry();
let schema = validator(®istry, RELEASE_SCHEMA);
let base: Value = serde_json::from_str(RELEASE_EXAMPLE).unwrap();
assert!(schema.is_valid(&base) && types_accept_release(&base));
let mutations: Vec<(&str, Box<dyn Fn(&mut Value)>)> = vec![
(
"an unknown top-level field",
Box::new(|value: &mut Value| {
value["surprise"] = json!("unexpected");
}),
),
(
"an unknown nested field",
Box::new(|value: &mut Value| {
value["archive"]["surprise"] = json!(1);
}),
),
(
"a missing required field",
Box::new(|value: &mut Value| {
value.as_object_mut().unwrap().remove("provenance");
}),
),
(
"a missing required nested field",
Box::new(|value: &mut Value| {
value["archive"].as_object_mut().unwrap().remove("sha256");
}),
),
(
"a schema version from another format revision",
Box::new(|value: &mut Value| {
value["schemaVersion"] = json!(3);
}),
),
(
"a kind outside the release namespace rule",
Box::new(|value: &mut Value| {
value["kind"] = json!("Scrollcase.Box.Release");
}),
),
(
"a kind naming another document type",
Box::new(|value: &mut Value| {
value["kind"] = json!("scrollcase.box.channel");
}),
),
(
"an identifier that is not lowercase dotted",
Box::new(|value: &mut Value| {
value["boxId"] = json!("Hello_Box");
}),
),
(
"an archive digest that is not SHA-256",
Box::new(|value: &mut Value| {
value["archive"]["sha256"] = json!("not-a-digest");
}),
),
(
"an uppercase archive digest",
Box::new(|value: &mut Value| {
value["archive"]["sha256"] = json!("A".repeat(64));
}),
),
(
"a zero archive size",
Box::new(|value: &mut Value| {
value["archive"]["sizeBytes"] = json!(0);
}),
),
(
"a zero installed size",
Box::new(|value: &mut Value| {
value["installedSizeBytes"] = json!(0);
}),
),
(
"an empty self-test import list",
Box::new(|value: &mut Value| {
value["selfTest"]["pythonImports"] = json!([]);
}),
),
(
"a builder revision that is not a commit",
Box::new(|value: &mut Value| {
value["provenance"]["builderRevision"] = json!("abc");
}),
),
(
"weights without assets",
Box::new(|value: &mut Value| {
value["weights"] = json!("on-demand");
value.as_object_mut().unwrap().remove("assets");
}),
),
(
"assets without weights",
Box::new(|value: &mut Value| {
value.as_object_mut().unwrap().remove("weights");
value["assets"] = json!([{
"url": "https://example.invalid/w.bin",
"relativePath": "weights/w.bin",
"sizeBytes": 1,
"sha256": "a".repeat(64),
}]);
}),
),
(
"an execution kind the format does not define",
Box::new(|value: &mut Value| {
value["execution"] = json!({ "kind": "shell", "command": "sh" });
}),
),
(
"an execution module carrying command-line syntax",
Box::new(|value: &mut Value| {
value["execution"] =
json!({ "kind": "python-module", "module": "a; rm -rf /", "defaultArgs": [] });
}),
),
(
"a target outside the supported matrix",
Box::new(|value: &mut Value| {
value["target"] = json!({
"platform": "solaris", "arch": "sparc", "accelerator": "cpu",
});
}),
),
(
"a host environment the format does not define",
Box::new(|value: &mut Value| {
value["compatibility"]["hostEnvironments"] = json!(["docker"]);
}),
),
(
"a defined compatibility constraint carrying the wrong type",
Box::new(|value: &mut Value| {
value["compatibility"]["minRamGb"] = json!("plenty");
}),
),
];
for (name, mutate) in mutations {
let mut mutated = base.clone();
mutate(&mut mutated);
let schema_accepts = schema.is_valid(&mutated);
let types_accept = types_accept_release(&mutated);
assert_eq!(
schema_accepts, types_accept,
"{name}: the canonical schema and the typed check disagree \
(schema accepted: {schema_accepts}, types accepted: {types_accept})"
);
assert!(!schema_accepts, "{name}: the mutation was supposed to be invalid");
}
}
#[test]
fn a_compatibility_constraint_the_format_does_not_define_is_carried_not_refused() {
let registry = registry();
let schema = validator(®istry, RELEASE_SCHEMA);
let mut release: Value = serde_json::from_str(RELEASE_EXAMPLE).unwrap();
release["compatibility"]["org.example.minVramGb"] = json!(24);
assert!(schema.is_valid(&release), "the canonical schema leaves compatibility open");
assert!(
types_accept_release(&release),
"the types refused a constraint the schema they mirror accepts"
);
let parsed: ReleaseManifest = serde_json::from_value(release.clone()).unwrap();
assert_eq!(
parsed.compatibility.additional.get("org.example.minVramGb"),
Some(&json!(24))
);
assert!(parsed.compatibility.min_ram_gb.is_some(), "a defined constraint stays typed");
assert_eq!(
serde_json::to_value(&parsed.compatibility).unwrap()["org.example.minVramGb"],
json!(24),
"an unknown constraint must survive a round trip unchanged"
);
}