weavatrix-refactor-plan 0.1.1

Evidence metadata, validation profiles, and canonical fingerprints for Weavatrix refactor plans
Documentation
use weavatrix_refactor_plan::{RefactorPlanLimits, parse_refactor_plan};

const PREFIX: &str = r#"{"schemaVersion":"weavatrix.refactor-plan.v1","operation":"create","operations":[{"kind":"create","value":{"path":"a.rs","contents":"x"}}]"#;

#[test]
fn bounded_parser_accepts_a_valid_plan() {
    let source = format!("{PREFIX}}}");
    let plan = parse_refactor_plan(source.as_bytes(), RefactorPlanLimits::default()).unwrap();
    assert_eq!(plan.operation, "create");
}

#[test]
fn duplicate_names_fail_at_top_operation_evidence_and_extension_depths() {
    let cases = [
        r#"{"schemaVersion":"weavatrix.refactor-plan.v1","schemaVersion":"weavatrix.refactor-plan.v1","operation":"x","operations":[{"kind":"create","value":{"path":"a.rs","contents":"x"}}]}"#,
        r#"{"schemaVersion":"weavatrix.refactor-plan.v1","operation":"x","operations":[{"kind":"create","value":{"path":"a.rs","path":"b.rs","contents":"x"}}]}"#,
        r#"{"schemaVersion":"weavatrix.refactor-plan.v1","operation":"x","operations":[{"kind":"create","value":{"path":"a.rs","contents":"x"}}],"completenessProof":{"scope":{"kind":"REPOSITORY","value":"root"},"planner":{"name":"p","name":"q","version":"1","backend":"b"}}}"#,
        r#"{"schemaVersion":"weavatrix.refactor-plan.v1","operation":"x","operations":[{"kind":"create","value":{"path":"a.rs","contents":"x","future":{"same":1,"same":2}}}]}"#,
    ];
    for source in cases {
        let error =
            parse_refactor_plan(source.as_bytes(), RefactorPlanLimits::default()).unwrap_err();
        assert!(error.message().contains("duplicate JSON object member"));
    }
}

#[test]
fn raw_unsafe_numbers_are_rejected_after_duplicate_safe_parse() {
    for number in ["-0.0", "9007199254740992", "9007199254740993"] {
        let source = format!("{PREFIX},\"futureNumber\":{number}}}");
        assert!(parse_refactor_plan(source.as_bytes(), RefactorPlanLimits::default()).is_err());
    }
}

#[test]
fn malformed_recognized_evidence_is_not_demoted_to_an_extension() {
    let source = format!("{PREFIX},\"warnings\":\"not-an-array\"}}");
    assert!(parse_refactor_plan(source.as_bytes(), RefactorPlanLimits::default()).is_err());
}

#[test]
fn non_extensible_operation_wrappers_and_permissions_reject_unknown_fields() {
    for source in [
        r#"{"schemaVersion":"weavatrix.refactor-plan.v1","operation":"x","operations":[{"kind":"create","value":{"path":"a.rs","contents":"x"},"ignored":true}]}"#,
        r#"{"schemaVersion":"weavatrix.refactor-plan.v1","operation":"x","operations":[{"kind":"create","value":{"path":"a.rs","contents":"x","permissions":{"executable":false,"ignored":true}}}]}"#,
    ] {
        assert!(parse_refactor_plan(source.as_bytes(), RefactorPlanLimits::default()).is_err());
    }
}

#[test]
fn serialized_input_limit_is_checked_before_json_allocation() {
    let limits = RefactorPlanLimits {
        max_total_create_bytes: 1,
        max_create_bytes_per_file: 1,
        max_total_text_bytes: 1,
        max_extension_bytes: 1,
        max_evidence_text_bytes: 1,
        max_paths: 1,
        max_path_bytes: 1,
        ..RefactorPlanLimits::default()
    };
    let oversized = vec![b' '; 1024 * 1024 + 7];
    assert!(parse_refactor_plan(&oversized, limits).is_err());
}

#[test]
fn json_member_name_limit_accepts_4096_and_rejects_4097_bytes() {
    for (length, accepted) in [(4096, true), (4097, false)] {
        let key = "k".repeat(length);
        let source = format!("{PREFIX},\"{key}\":null}}");
        assert_eq!(
            parse_refactor_plan(source.as_bytes(), RefactorPlanLimits::default()).is_ok(),
            accepted
        );
    }
}

#[test]
fn duplicate_huge_key_error_does_not_echo_attacker_input() {
    let key = "attacker".repeat(6_000);
    let source = format!("{PREFIX},\"{key}\":1,\"{key}\":2}}");
    let limits = RefactorPlanLimits {
        max_path_bytes: key.len(),
        ..RefactorPlanLimits::default()
    };
    let error = parse_refactor_plan(source.as_bytes(), limits).unwrap_err();
    assert!(error.message().contains("duplicate JSON object member"));
    assert!(error.message().len() < 256);
}