Files
ARCADIA-Studio/crates/oa-desktop/src/document_alias_rename_tests.rs
T

460 lines
18 KiB
Rust

use super::*;
fn fixture() -> Document {
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
("project.arcadia".into(), r#"arcadia 7;
project mission "Испытание" id "10000000-0000-4000-8000-000000000001" {
include "work.arcadia"; include "views.arcadia";
}
"#.replace('\n', "\r\n")),
("work.arcadia".into(), r#"// @assess and assess are explanatory prose.
oa_activity assess "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000002" { description "Keep @assess and assess"; }
oa_activity assist "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000003" { parent @ // owner evidence
assess; }
sa_function service "Service" id "10000000-0000-4000-8000-000000000004" {}
interaction transfer "Transfer" id "10000000-0000-4000-8000-000000000005" { source @assess; target @assist; }
trace review "Reviewed correspondence" id "10000000-0000-4000-8000-000000000006" { oa @assess; sa @service; kind Refinement; baseline { oa { name "Old assess"; description "@assess"; }; sa { name "Old service"; description "Kept"; }; }; }
artifact evidence "Evidence" id "10000000-0000-4000-8000-000000000007" { owner @review; uri "docs/assess.md"; }
"#.replace('\n', "\r\n")),
("views.arcadia".into(), r#"diagram overview "Overview" id "10000000-0000-4000-8000-000000000008" { layer Oa; routes [{ edge @ // routed identity
transfer; waypoints [{ x 4.20e2; y -16; }]; }]; }
node placement id "10000000-0000-4000-8000-000000000009" { diagram @overview; element @assess; position { x -0.0; y 12.375; }; size { width 260; height 94; }; }
"#.replace('\n', "\r\n")),
]),
};
let document = Document::from_bundle(bundle.clone(), ProjectLimits::default()).unwrap();
assert_eq!(
document.bundle(),
&bundle,
"fixture must need no migration or ID insertion"
);
document
}
fn id(number: u8) -> Id {
Id::parse_str(&format!("10000000-0000-4000-8000-{number:012}")).unwrap()
}
fn prepare(document: &Document, target: Id, replacement: &str) -> PreparedAliasRename {
document
.alias_rename_request(target, replacement.into())
.unwrap()
.prepare()
.unwrap()
}
// Capture every live Document state relevant to refusal/no-op. Arc identity also
// catches replacing an equal snapshot or saved marker behind the user's back.
struct Unchanged {
current: Arc<CompiledSourceProject>,
saved: Option<Arc<SourceBundle>>,
past: Vec<(Arc<CompiledSourceProject>, usize)>,
future: Vec<(Arc<CompiledSourceProject>, usize)>,
revision: u64,
current_bytes: usize,
recovered: bool,
}
impl Unchanged {
fn capture(document: &Document) -> Self {
Self {
current: document.snapshot(),
saved: document.saved.clone(),
past: document
.past
.iter()
.map(|h| (h.snapshot.clone(), h.bytes))
.collect(),
future: document
.future
.iter()
.map(|h| (h.snapshot.clone(), h.bytes))
.collect(),
revision: document.revision,
current_bytes: document.current_bytes,
recovered: document.recovered_unsaved,
}
}
fn assert(&self, document: &Document) {
assert!(Arc::ptr_eq(&self.current, &document.current));
match (&self.saved, &document.saved) {
(Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)),
(None, None) => {}
_ => panic!("saved baseline changed"),
}
for (before, after) in [
(&self.past, &document.past),
(&self.future, &document.future),
] {
assert_eq!(before.len(), after.len());
for ((snapshot, bytes), entry) in before.iter().zip(after) {
assert!(Arc::ptr_eq(snapshot, &entry.snapshot));
assert_eq!(*bytes, entry.bytes);
}
}
assert_eq!(self.revision, document.revision);
assert_eq!(self.current_bytes, document.current_bytes);
assert_eq!(self.recovered, document.recovered_unsaved);
}
}
#[test]
fn each_identity_family_changes_only_authored_alias_roles_and_one_history_entry() {
// Literal role-specific replacements are independent of the updater/tokenizer.
// They deliberately exclude prose, historical review text and URI spelling.
type AliasCase<'a> = (u8, &'a str, &'a str, &'a [(&'a str, &'a str)]);
let cases: &[AliasCase<'_>] = &[
(
1,
"mission",
"renamed",
&[("project mission ", "project renamed ")],
),
(
2,
"assess",
"renamed",
&[
("oa_activity assess ", "oa_activity renamed "),
("\r\n assess;", "\r\n renamed;"),
("@assess;", "@renamed;"),
],
),
(
5,
"transfer",
"renamed",
&[
("interaction transfer ", "interaction renamed "),
("\r\n transfer;", "\r\n renamed;"),
],
),
(
6,
"review",
"renamed",
&[
("trace review ", "trace renamed "),
("owner @review;", "owner @renamed;"),
],
),
(
7,
"evidence",
"renamed",
&[("artifact evidence ", "artifact renamed ")],
),
(
8,
"overview",
"renamed",
&[
("diagram overview ", "diagram renamed "),
("diagram @overview;", "diagram @renamed;"),
],
),
(
9,
"placement",
"renamed",
&[("node placement ", "node renamed ")],
),
];
for &(number, old, new, replacements) in cases {
let mut document = fixture();
let original = document.snapshot();
let mut expected_model = document.project().clone();
// Explicit per-family oracle, separate from production expectation helper.
match number {
1 => expected_model.alias = new.into(),
2 => expected_model.elements.get_mut(&id(2)).unwrap().alias = new.into(),
5 => expected_model.relations.get_mut(&id(5)).unwrap().alias = new.into(),
6 => expected_model.traces.get_mut(&id(6)).unwrap().alias = new.into(),
7 => expected_model.artifact_links.get_mut(&id(7)).unwrap().alias = new.into(),
8 => expected_model.diagrams.get_mut(&id(8)).unwrap().alias = new.into(),
9 => {
expected_model
.diagrams
.get_mut(&id(8))
.unwrap()
.nodes
.get_mut(&id(9))
.unwrap()
.alias = new.into()
}
_ => unreachable!(),
}
let mut expected_bundle = document.bundle().clone();
for source in expected_bundle.files.values_mut() {
for &(before, after) in replacements {
*source = source.replace(before, after);
}
}
let state = Unchanged::capture(&document);
let prepared = prepare(&document, id(number), new);
state.assert(&document);
assert!(prepared.changed());
assert_eq!(prepared.old_alias(), old);
assert_eq!(prepared.new_alias(), new);
assert_eq!(prepared.target(), id(number));
assert_eq!(prepared.revision(), 0);
assert_eq!(prepared.before().bundle, original.bundle);
assert_eq!(prepared.after().bundle, expected_bundle);
assert_eq!(prepared.after().decoded.project, expected_model);
assert!(document.commit_alias_rename(&prepared).unwrap());
assert!(
Arc::ptr_eq(&document.current, &prepared.0.after),
"adopt the exact prepared Arc"
);
assert_eq!(document.past.len(), 1);
assert!(document.dirty());
let after = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&prepared.clone()).is_err());
after.assert(&document);
assert!(document.undo());
assert!(Arc::ptr_eq(&document.current, &original));
assert!(!document.dirty());
assert!(!document.can_undo());
assert!(document.redo());
assert_eq!(document.bundle(), &expected_bundle);
assert_eq!(document.project(), &expected_model);
document.mark_saved(document.snapshot());
let reopened =
Document::from_bundle(document.bundle().clone(), ProjectLimits::default()).unwrap();
assert_eq!(reopened.project(), &expected_model);
assert_eq!(reopened.bundle(), &expected_bundle);
assert!(!reopened.dirty());
}
}
#[test]
fn highlighted_keyword_alias_is_valid_without_normalizing_input() {
assert!(matches!(
language::tokenize("oa_activity")[0].class,
language::TokenClass::Keyword
));
let mut document = fixture();
let prepared = prepare(&document, id(2), "oa_activity");
document.commit_alias_rename(&prepared).unwrap();
assert_eq!(document.project().elements[&id(2)].alias, "oa_activity");
assert_eq!(document.project().elements[&id(2)].name, "Оценка ущерба");
}
#[test]
fn invalid_and_identical_aliases_preserve_live_state_and_redo() {
let mut document = fixture();
let first = prepare(&document, id(2), "changed");
document.commit_alias_rename(&first).unwrap();
assert!(document.undo());
document.invalidate_saved_baseline();
document.mark_recovered();
let state = Unchanged::capture(&document);
for replacement in ["", "has space", "project", "assist", " assess", "assess "] {
assert!(
document
.alias_rename_request(id(2), replacement.into())
.unwrap()
.prepare()
.is_err()
);
state.assert(&document);
}
assert!(
document
.alias_rename_request(Id::nil(), "new".into())
.is_err()
);
let no_op = prepare(&document, id(2), "assess");
assert!(!no_op.changed());
assert!(!document.commit_alias_rename(&no_op).unwrap());
state.assert(&document);
assert!(document.redo());
assert_eq!(document.project().elements[&id(2)].alias, "changed");
}
#[test]
fn equal_bytes_undo_replay_cross_document_and_exhausted_revision_refuse() {
let mut document = fixture();
let old = prepare(&document, id(2), "renamed");
let mut second = fixture();
let second_state = Unchanged::capture(&second);
assert!(second.commit_alias_rename(&old).is_err());
second_state.assert(&second);
// Even an identical shared accepted Arc cannot confer this Document's owner.
second.current = document.current.clone();
let second_state = Unchanged::capture(&second);
assert!(second.commit_alias_rename(&old).is_err());
second_state.assert(&second);
document
.execute(Command::Rename(id(2), "Later name".into()))
.unwrap();
let edited = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&old).is_err());
edited.assert(&document);
assert!(document.undo());
assert!(Arc::ptr_eq(&document.current, &old.0.request.before));
let undone = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&old).is_err());
undone.assert(&document);
document.revision = u64::MAX - 1;
let last = prepare(&document, id(2), "renamed");
document
.execute(Command::Rename(id(2), "Exhaust revision".into()))
.unwrap();
document.undo();
assert!(Arc::ptr_eq(&document.current, &last.0.request.before));
let exhausted = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&last).is_err());
assert!(
document
.alias_rename_request(id(2), "renamed".into())
.is_err()
);
assert!(
document
.alias_rename_request(id(2), "assess".into())
.is_err()
);
exhausted.assert(&document);
}
#[test]
fn preview_retains_latest_saved_baseline_and_recovery_state() {
for baseline in ["saved_before", "unknown", "raw_repair", "recovered"] {
let mut document = fixture();
let prepared = prepare(&document, id(2), "renamed");
let original = document.snapshot();
match baseline {
"saved_before" => document.mark_saved(original.clone()),
"unknown" => document.invalidate_saved_baseline(),
"raw_repair" => {
let mut raw = document.bundle().clone();
raw.files
.insert("project.arcadia".into(), "invalid repair source".into());
document.bind_repair_baseline(raw);
}
"recovered" => document.mark_recovered(),
_ => unreachable!(),
}
let saved = document.saved.clone();
let recovered = document.recovered_unsaved;
assert!(document.commit_alias_rename(&prepared).unwrap());
assert_eq!(document.saved, saved);
assert_eq!(document.recovered_unsaved, recovered);
assert!(document.dirty());
assert!(document.undo());
assert_eq!(document.saved, saved);
assert_eq!(document.recovered_unsaved, recovered);
assert_eq!(document.dirty(), baseline != "saved_before");
}
// A completed save after Preview remains authoritative, including when it
// clears recovery; a worker must not restore its earlier state on Confirm.
let mut document = fixture();
document.mark_recovered();
document.invalidate_saved_baseline();
let prepared = prepare(&document, id(2), "renamed");
document.mark_saved(document.snapshot());
document.commit_alias_rename(&prepared).unwrap();
assert!(!document.recovered_unsaved);
document.undo();
assert!(!document.dirty());
// Undo from saved C to B, then a new B->C rename can correctly finish clean.
document.redo();
document.mark_saved(document.snapshot());
document.undo();
let return_to_saved = prepare(&document, id(2), "renamed");
document.commit_alias_rename(&return_to_saved).unwrap();
assert!(!document.dirty());
assert!(!document.can_redo());
}
#[test]
fn mutable_public_preview_copies_cannot_change_prepared_authority() {
let mut document = fixture();
let prepared = prepare(&document, id(2), "renamed");
let exact = prepared.after().clone();
let mut counterfeit = prepared.after().clone();
counterfeit.decoded.project.name = "Injected unrelated edit".into();
counterfeit.index.declarations.clear();
let unchanged = Unchanged::capture(&document);
assert!(document.apply_compiled(counterfeit).is_err());
unchanged.assert(&document);
let mut unrelated = exact.decoded.project.clone();
unrelated.name = "A self-consistent unrelated candidate".into();
let forged_dto = source_project::compile_bundle(
&source_project::format_bundle(&unrelated),
ProjectLimits::default(),
)
.unwrap();
assert_ne!(forged_dto.decoded.project, prepared.after().decoded.project);
// No API converts either DTO to PreparedAliasRename. Mutating owned copies
// leaves the opaque capability and its exact adopted model/source intact.
document.commit_alias_rename(&prepared).unwrap();
assert_eq!(document.project(), &exact.decoded.project);
assert_eq!(document.bundle(), &exact.bundle);
}
#[test]
fn exact_history_capacity_admits_immediate_undo_and_rejects_larger_candidate() {
let mut document = fixture();
let mut bundle = document.bundle().clone();
let padding = HISTORY_BYTES / 2 - document.current_bytes;
bundle
.files
.get_mut("views.arcadia")
.unwrap()
.push_str(&format!("//{}\r\n", "x".repeat(padding - 4)));
document.apply_bundle(bundle).unwrap();
assert_eq!(document.current_bytes, HISTORY_BYTES / 2);
// A same-length alias retains the exact estimate; older history is evictable.
let boundary = prepare(&document, id(2), "rating");
assert_eq!(
boundary.0.before_bytes + boundary.0.after_bytes,
HISTORY_BYTES
);
let unchanged = Unchanged::capture(&document);
assert!(
document
.alias_rename_request(id(2), "longer_alias".into())
.unwrap()
.prepare()
.is_err()
);
unchanged.assert(&document);
document.commit_alias_rename(&boundary).unwrap();
assert_eq!(document.past.len(), 1);
assert!(document.undo());
assert!(Arc::ptr_eq(&document.current, &boundary.0.request.before));
assert!(!document.can_undo());
assert!(document.redo());
assert!(Arc::ptr_eq(&document.current, &boundary.0.after));
assert!(check_history_capacity(usize::MAX, 1).is_err());
assert!(check_history_capacity(HISTORY_BYTES, 1).is_err());
}
#[test]
fn mint_and_commit_refuse_exhausted_or_inconsistent_live_history_estimate() {
let mut document = fixture();
let prepared = prepare(&document, id(2), "renamed");
// Inject cache corruption to falsify the commit-side check independently of
// the preparation admission. Normal edits also change revision and Arc.
document.current_bytes = HISTORY_BYTES;
let unchanged = Unchanged::capture(&document);
assert!(
document
.alias_rename_request(id(2), "renamed".into())
.is_err()
);
assert!(document.commit_alias_rename(&prepared).is_err());
// No-op intent needs no Undo reservation even at the admission boundary.
assert!(
document
.alias_rename_request(id(2), "assess".into())
.is_ok()
);
unchanged.assert(&document);
}