boxology-cli-core 0.1.1

Reusable command-line boundary
Documentation
use boxology_cli_core::{
    CheckClassificationError, ClassifyStepError, DuplicatePackages, PackageSchemas, classify_step,
};
use boxology_contract::BoxId;
use boxology_generator::{OUTPUTS, generate};
use boxology_generator_model::GenerationRequest;
use boxology_schema::{SchemaDocument, SchemaPayload, SchemaVariant};
use boxology_workspace::{
    CheckReport, CheckStatus, Completion, ContractClassificationCompletion,
    DiffOwnershipCompletion, ExternalOutput,
};

const V1: &str = "boxology::contract! { #[error] pub enum GreetError { EmptyName } #[capability(exposure=external)] pub async fn greet(name:String)->Result<String,GreetError>; }";
const V2: &str = "boxology::contract! { #[error] pub enum GreetError { EmptyName, Other } #[capability(exposure=external)] pub async fn greet(name:String)->Result<String,GreetError>; }";
const OTHER: &str = "boxology::contract! { #[error] pub enum GreetError { EmptyName } #[capability(exposure=external)] pub async fn wave(name:String)->Result<String,GreetError>; }";

fn schema_bytes(box_id: &str, source: &str) -> Vec<u8> {
    let request = GenerationRequest::new(
        BoxId::new(box_id).unwrap(),
        "src/lib.rs".to_owned(),
        vec![
            (
                "boxology.toml".to_owned(),
                format!("schema = 1\nid = \"{box_id}\"\nkind = \"box\"\n").into_bytes(),
            ),
            ("src/lib.rs".to_owned(), source.as_bytes().to_vec()),
        ],
        Vec::new(),
        OUTPUTS.iter().map(|path| (*path).to_owned()).collect(),
    )
    .unwrap();
    generate(request)
        .unwrap()
        .files()
        .iter()
        .find(|file| file.path() == "generated/schema.json")
        .expect("schema.json is a fixed generator output")
        .bytes()
        .to_vec()
}

fn parse(bytes: &[u8]) -> SchemaDocument {
    SchemaDocument::parse(bytes).unwrap()
}

fn with_other_revision(mut document: SchemaDocument) -> SchemaDocument {
    let other = parse(&schema_bytes("hello", OTHER));
    assert_ne!(document.revision, other.revision);
    document.revision = other.revision;
    document
}

fn variant(name: &str) -> SchemaVariant {
    SchemaVariant {
        name: name.to_owned(),
        docs: Vec::new(),
        deprecation: None,
        payload: SchemaPayload::Unit,
    }
}

fn pkg(name: &str, base: Option<Vec<u8>>, submitted: Vec<u8>) -> PackageSchemas {
    PackageSchemas::new(BoxId::new(name).unwrap(), base, submitted)
}

fn coded(
    result: Result<ContractClassificationCompletion, ClassifyStepError>,
) -> CheckClassificationError {
    match result {
        Err(ClassifyStepError::Classification(error)) => error,
        other => panic!("expected classification error, got {other:?}"),
    }
}

fn report(contract_classification: ContractClassificationCompletion) -> CheckReport {
    CheckReport {
        discovery: Completion::Passed,
        regeneration: Completion::Passed,
        contract_classification,
        diff_ownership: DiffOwnershipCompletion::Passed,
        cargo_graph: Completion::Passed,
        fmt: Completion::Passed,
        clippy: Completion::Passed,
        tests: Completion::Passed,
        quality: Completion::Passed,
        external_output: ExternalOutput::empty(),
    }
}

#[test]
fn equal_revision_tamper_is_bxw0082_with_bxc0037() {
    let base = parse(&schema_bytes("hello", V1));
    let mut equal = base.clone();
    equal.types[0].variants.push(variant("Other"));
    let error = coded(classify_step(&[pkg(
        "hello",
        Some(base.canonical_bytes()),
        equal.canonical_bytes(),
    )]));
    assert_eq!(error.code(), "BXW0082");
    assert_eq!(error.side(), "pairing");
    assert_eq!(
        error.detail(),
        "the base-revision and checked-in schema documents must pair and satisfy classifier integrity"
    );
    assert!(error.diagnostics().to_string().contains("BXC0037"));
}

#[test]
fn revision_only_difference_is_bxw0082_with_bxc0038() {
    let base = parse(&schema_bytes("hello", V1));
    let silence = with_other_revision(base.clone());
    let error = coded(classify_step(&[pkg(
        "hello",
        Some(base.canonical_bytes()),
        silence.canonical_bytes(),
    )]));
    assert_eq!(error.code(), "BXW0082");
    assert!(error.diagnostics().to_string().contains("BXC0038"));
}

#[test]
fn variant_addition_preserves_condition_in_payload_and_render() {
    let completion = classify_step(&[pkg(
        "hello",
        Some(schema_bytes("hello", V1)),
        schema_bytes("hello", V2),
    )])
    .unwrap();
    let ContractClassificationCompletion::Failed(findings) = &completion else {
        panic!("expected Failed");
    };
    assert_eq!(
        findings.as_slice()[0].condition(),
        Some("unknown-variant tolerance")
    );
    assert!(
        findings
            .to_string()
            .contains("condition=\"unknown-variant tolerance\"")
    );
    assert!(
        report(completion)
            .render_human()
            .contains("condition=\"unknown-variant tolerance\"")
    );
}

#[test]
fn findings_sort_by_package_path_then_code_from_reverse_input() {
    let mut alpha_base = parse(&schema_bytes("alpha", V1));
    alpha_base.types[0].variants.push(variant("Other"));
    let mut alpha_submitted = parse(&schema_bytes("alpha", V1));
    alpha_submitted.types[0]
        .docs
        .push("Extra type docs.".to_owned());
    let alpha_submitted = with_other_revision(alpha_submitted);
    let ContractClassificationCompletion::Failed(findings) = classify_step(&[
        pkg(
            "zeta",
            Some(schema_bytes("zeta", V2)),
            schema_bytes("zeta", V1),
        ),
        pkg(
            "alpha",
            Some(alpha_base.canonical_bytes()),
            alpha_submitted.canonical_bytes(),
        ),
    ])
    .unwrap() else {
        panic!("expected Failed");
    };
    let keys: Vec<_> = findings
        .as_slice()
        .iter()
        .map(|f| (f.package().as_str(), f.path(), f.code()))
        .collect();
    assert_eq!(
        keys,
        [
            ("alpha", "alpha/type/GreetError", "BXC0033"),
            ("alpha", "alpha/type/GreetError/variant/Other", "BXC0035"),
            ("zeta", "zeta/type/GreetError/variant/Other", "BXC0035"),
        ]
    );
}

#[test]
fn reverse_incompatible_is_failed_payload() {
    let ContractClassificationCompletion::Failed(findings) = classify_step(&[pkg(
        "hello",
        Some(schema_bytes("hello", V2)),
        schema_bytes("hello", V1),
    )])
    .unwrap() else {
        panic!("expected Failed");
    };
    assert_eq!(findings.as_slice()[0].code(), "BXC0035");
}

#[test]
fn equal_modulo_provenance_is_passed_with_no_finding_lines() {
    let bytes = schema_bytes("hello", V1);
    let completion = classify_step(&[pkg("hello", Some(bytes.clone()), bytes)]).unwrap();
    assert_eq!(completion, ContractClassificationCompletion::Passed);
    assert_eq!(
        report(completion)
            .render_human()
            .lines()
            .filter(|line| line.starts_with("  BXC"))
            .count(),
        0
    );
}

#[test]
fn classification_findings_do_not_affect_exit_code() {
    let completion = classify_step(&[pkg(
        "hello",
        Some(schema_bytes("hello", V1)),
        schema_bytes("hello", V2),
    )])
    .unwrap();
    let report = report(completion);
    assert_eq!(report.status(), CheckStatus::Passed);
    assert_eq!(report.exit_code(), 0);
}

#[test]
fn two_findings_on_one_package_are_both_kept() {
    let mut base = parse(&schema_bytes("hello", V1));
    base.types[0].variants.push(variant("Other"));
    let mut submitted = parse(&schema_bytes("hello", V1));
    submitted.types[0].docs.push("Extra type docs.".to_owned());
    let submitted = with_other_revision(submitted);
    let ContractClassificationCompletion::Failed(findings) = classify_step(&[pkg(
        "hello",
        Some(base.canonical_bytes()),
        submitted.canonical_bytes(),
    )])
    .unwrap() else {
        panic!("expected Failed");
    };
    assert_eq!(
        findings
            .as_slice()
            .iter()
            .map(|f| f.code())
            .collect::<Vec<_>>(),
        ["BXC0033", "BXC0035"]
    );
    assert!(findings.to_string().contains("BXC0033"));
    assert!(findings.to_string().contains("BXC0035"));
}

#[test]
fn parse_and_duplicate_errors_are_immediate() {
    assert_eq!(
        coded(classify_step(&[pkg(
            "hello",
            Some(b"{".to_vec()),
            schema_bytes("hello", V1),
        )]))
        .code(),
        "BXW0080"
    );
    assert_eq!(
        coded(classify_step(&[pkg(
            "hello",
            Some(schema_bytes("hello", V1)),
            b"{".to_vec(),
        )]))
        .code(),
        "BXW0081"
    );
    let bytes = schema_bytes("hello", V1);
    assert!(matches!(
        classify_step(&[
            pkg("hello", Some(bytes.clone()), bytes.clone()),
            pkg("hello", Some(bytes.clone()), bytes),
        ])
        .unwrap_err(),
        ClassifyStepError::Duplicate(DuplicatePackages)
    ));
}