use weavatrix_refactor_plan::{
EDIT_PLAN_SCHEMA, EditPlan, PlanErrorCode, PlanEvidence, RefactorPlan,
weavatrix_edit::DeclaredEditPlan,
};
fn annotated_legacy_json(vendor_blob: &str) -> String {
let sha = "0".repeat(64);
format!(
r#"{{
"schemaVersion": "{EDIT_PLAN_SCHEMA}",
"operation": "rename_symbol",
"createdAt": "2026-08-02T12:00:00Z",
"graphRevision": "revision-1",
"vendorBlob": {vendor_blob},
"files": [{{
"path": "src/a.rs",
"sha256": "{sha}",
"language": "rust",
"edits": [{{
"startLine": 1, "startChar": 0, "endLine": 1, "endChar": 1,
"before": "a", "after": "b", "provenance": "EXACT_LSP"
}}]
}}]
}}"#
)
}
fn decode_both(json: &str) -> (EditPlan, EditPlan) {
let capturing: EditPlan = blazingly_json::from_str(json).expect("capturing decode");
let declared: EditPlan = blazingly_json::from_str::<DeclaredEditPlan>(json)
.expect("declared-only decode")
.into();
(capturing, declared)
}
#[test]
fn declared_only_decode_keeps_every_declared_member_and_drops_every_other() {
let (capturing, declared) = decode_both(&annotated_legacy_json(r#""vendor""#));
assert_eq!(capturing.operation, declared.operation);
assert_eq!(capturing.files.len(), declared.files.len());
assert_eq!(capturing.files[0].path, declared.files[0].path);
assert_eq!(capturing.files[0].sha256, declared.files[0].sha256);
assert_eq!(capturing.files[0].edits, declared.files[0].edits);
assert_eq!(
capturing
.validate()
.expect("capturing validates")
.total_edits(),
declared
.validate()
.expect("declared validates")
.total_edits(),
);
assert!(capturing.extensions.contains_key("createdAt"));
assert!(capturing.files[0].extensions.contains_key("language"));
assert!(declared.extensions.is_empty());
assert!(declared.files[0].extensions.is_empty());
}
#[test]
fn declared_only_decode_silently_erases_plan_evidence_on_conversion() {
let (capturing, declared) = decode_both(&annotated_legacy_json(r#""vendor""#));
let kept = RefactorPlan::from_text_edit_plan(capturing).expect("capturing conversion");
assert_eq!(
kept.evidence.created_at.as_deref(),
Some("2026-08-02T12:00:00Z")
);
assert_eq!(kept.evidence.graph_revision.as_deref(), Some("revision-1"));
assert!(kept.evidence.extensions.contains_key("vendorBlob"));
let lost = RefactorPlan::from_text_edit_plan(declared).expect("declared-only conversion");
assert_eq!(lost.evidence, PlanEvidence::default());
assert_ne!(
kept.fingerprint().expect("kept fingerprint"),
lost.fingerprint().expect("lost fingerprint"),
);
}
#[test]
fn declared_only_decode_would_make_the_extension_budget_vacuous() {
let blob = format!("[{}]", vec!["1"; 5_000].join(","));
let (capturing, declared) = decode_both(&annotated_legacy_json(&blob));
let kept = RefactorPlan::from_text_edit_plan(capturing).expect("capturing conversion");
let error = kept
.validate()
.expect_err("the extension node budget must reject the blob");
assert_eq!(error.code(), PlanErrorCode::EvidenceTooLarge);
let lost = RefactorPlan::from_text_edit_plan(declared).expect("declared-only conversion");
assert!(lost.validate().is_ok());
}