Files
ARCADIA-Studio/crates/oa-core/tests/source_files.rs
T
sorokin.oleg.edu 9fd76aef3a Add current-draft diagnostic underlines and passive hover
Preserve raw include diagnostic provenance and derive bands from the existing shaped editor. Retain deferred tooltip paragraphs through rendering. Include source, regression, failure and native-trial evidence; corrected native acceptance remains pending.
2026-09-13 01:43:22 -07:00

470 lines
16 KiB
Rust

use oa_core::{source_files::*, source_project::*};
use std::{collections::BTreeMap, fs};
fn setup() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap();
fs::write(
directory.path().join("project.arcadia"),
"arcadia 5; project p \"P\" { include \"parts/a.arcadia\"; }\n",
)
.unwrap();
fs::create_dir(directory.path().join("parts")).unwrap();
fs::write(
directory.path().join("parts/a.arcadia"),
"// rationale\noa_activity a \"A\" {}\n",
)
.unwrap();
directory
}
#[test]
fn loading_keeps_original_no_uuid_bytes_and_saving_stabilizes_exactly_once() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
let snapshot = load_project(&entry, ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics
.iter()
.all(|d| d.diagnostic.severity != oa_core::validation::Severity::Error)
);
let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
assert_ne!(compiled.bundle, snapshot.bundle);
for (path, expected) in &snapshot.expectations {
assert_eq!(
*expected,
DiskExpectation::Bytes(fs::read(directory.path().join(path)).unwrap())
);
}
let saved = save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap();
assert_eq!(saved.bundle, compiled.bundle);
let reopened = load_project(&entry, ProjectLimits::default()).unwrap();
assert_eq!(reopened.bundle, compiled.bundle);
assert_eq!(
compile_bundle(&reopened.reachable_bundle(), ProjectLimits::default())
.unwrap()
.decoded
.project,
compiled.decoded.project
);
assert_eq!(
save_project(&saved, &compiled, ProjectLimits::default())
.unwrap()
.bundle,
compiled.bundle
);
}
#[test]
fn invalid_and_missing_includes_keep_editable_text_and_located_diagnostics() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
fs::write(&entry, "arcadia 5; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
fs::write(
directory.path().join("parts/a.arcadia"),
"oa_activity a \"A\" { invalid",
)
.unwrap();
let snapshot = load_project(&entry, ProjectLimits::default()).unwrap();
assert_eq!(snapshot.bundle.files.len(), 2);
assert_eq!(
snapshot.expectations["missing.arcadia"],
DiskExpectation::Missing
);
assert!(
snapshot
.diagnostics
.iter()
.any(|d| d.path == "project.arcadia"
&& d.diagnostic.line == 2
&& d.diagnostic.code == "missing_include")
);
assert!(
snapshot
.diagnostics
.iter()
.any(|d| d.path == "parts/a.arcadia")
);
assert!(snapshot.bundle.files["parts/a.arcadia"].ends_with("invalid"));
}
#[test]
fn discovery_never_refreshes_drafts_and_preserves_detached_buffers() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
let original = load_project(&entry, ProjectLimits::default()).unwrap();
fs::write(directory.path().join("parts/a.arcadia"), "// external edit").unwrap();
fs::write(
directory.path().join("new.arcadia"),
"oa_activity b \"B\" {}",
)
.unwrap();
let overlays = BTreeMap::from([
(
"project.arcadia".into(),
"arcadia 5; project p \"P\" { include \"new.arcadia\"; }".into(),
),
(
"parts/a.arcadia".into(),
"// unfinished detached draft\noa_activity".into(),
),
]);
let discovered = discover_missing(&original, &overlays, ProjectLimits::default()).unwrap();
assert_eq!(
discovered.bundle.files["parts/a.arcadia"],
overlays["parts/a.arcadia"]
);
assert_eq!(
discovered.expectations["parts/a.arcadia"],
original.expectations["parts/a.arcadia"]
);
assert!(discovered.bundle.files.contains_key("new.arcadia"));
assert!(
!discovered
.reachable_bundle()
.files
.contains_key("parts/a.arcadia")
);
assert!(
discovered
.diagnostics
.iter()
.all(|d| d.diagnostic.severity != oa_core::validation::Severity::Error)
);
}
#[test]
fn unchanged_dependency_edits_and_deletions_block_save_before_any_write() {
for deletion in [false, true] {
let directory = setup();
let entry = directory.path().join("project.arcadia");
let snapshot = load_project(&entry, ProjectLimits::default()).unwrap();
let compiled =
compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
let snapshot = save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap();
let mut bundle = compiled.bundle.clone();
bundle
.files
.get_mut("project.arcadia")
.unwrap()
.push_str("// changed\n");
let changed = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
let child = directory.path().join("parts/a.arcadia");
if deletion {
fs::remove_file(&child).unwrap();
} else {
fs::write(&child, "// another editor").unwrap();
}
assert!(matches!(
save_project(&snapshot, &changed, ProjectLimits::default()),
Err(SourceFileError::Conflict(_))
));
assert_eq!(
fs::read_to_string(&entry).unwrap(),
compiled.bundle.files["project.arcadia"]
);
}
}
#[test]
fn new_destinations_require_explicit_missing_expectations_and_reject_collisions() {
let directory = tempfile::tempdir().unwrap();
let entry = directory.path().join("project.arcadia");
let mut snapshot = new_project(&entry, ProjectLimits::default()).unwrap();
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
(
"project.arcadia".into(),
"arcadia 5; project p \"P\" { include \"child.arcadia\"; }".into(),
),
("child.arcadia".into(), "oa_activity a \"A\" {}".into()),
]),
};
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
assert!(matches!(
save_project(&snapshot, &compiled, ProjectLimits::default()),
Err(SourceFileError::UnknownExpectation(_))
));
expect_new_file(&mut snapshot, "child.arcadia", ProjectLimits::default()).unwrap();
fs::write(
directory.path().join("child.arcadia"),
"// pre-existing work",
)
.unwrap();
assert!(matches!(
save_project(&snapshot, &compiled, ProjectLimits::default()),
Err(SourceFileError::Conflict(_))
));
assert!(!entry.exists());
assert!(expect_new_file(&mut snapshot, "child.arcadia", ProjectLimits::default()).is_err());
}
#[test]
fn aggregate_budgets_and_portable_paths_are_checked_before_loading_excess_data() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
let mut limits = ProjectLimits {
max_files: 1,
..ProjectLimits::default()
};
let snapshot = load_project(&entry, limits).unwrap();
assert_eq!(snapshot.bundle.files.len(), 1);
assert!(
snapshot
.diagnostics
.iter()
.any(|d| d.diagnostic.code == "project_limit")
);
limits.max_files = 256;
limits.model.max_source_bytes = fs::metadata(&entry).unwrap().len() as usize + 5;
let snapshot = load_project(&entry, limits).unwrap();
assert_eq!(snapshot.bundle.files.len(), 1);
assert!(
snapshot
.diagnostics
.iter()
.any(|d| d.diagnostic.code == "source_limit")
);
let mut snapshot = new_project(
&directory.path().join("new.arcadia"),
ProjectLimits::default(),
)
.unwrap();
for invalid in [
"../escape.arcadia",
"a\\b.arcadia",
"CON.arcadia",
"/absolute.arcadia",
"a/../b.arcadia",
".arcadia-save-project.arcadia.journal/intrusion.arcadia",
] {
assert!(
expect_new_file(&mut snapshot, invalid, ProjectLimits::default()).is_err(),
"{invalid}"
);
}
}
#[test]
fn physical_hard_link_aliases_are_rejected() {
let directory = setup();
fs::hard_link(
directory.path().join("parts/a.arcadia"),
directory.path().join("alias.arcadia"),
)
.unwrap();
fs::write(
directory.path().join("project.arcadia"),
"arcadia 5; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
)
.unwrap();
assert!(matches!(
load_project(
&directory.path().join("project.arcadia"),
ProjectLimits::default()
),
Err(SourceFileError::Path(_))
));
}
#[cfg(unix)]
#[test]
fn symlinks_to_outside_or_inside_project_are_rejected() {
let directory = setup();
let outside = tempfile::tempdir().unwrap();
fs::write(
outside.path().join("outside.arcadia"),
"oa_activity outside \"Outside\" {}",
)
.unwrap();
for destination in [
outside.path().join("outside.arcadia"),
directory.path().join("parts/a.arcadia"),
] {
let link = directory.path().join("alias.arcadia");
std::os::unix::fs::symlink(&destination, &link).unwrap();
fs::write(
directory.path().join("project.arcadia"),
"arcadia 5; project p \"P\" { include \"alias.arcadia\"; }",
)
.unwrap();
let loaded = load_project(
&directory.path().join("project.arcadia"),
ProjectLimits::default(),
)
.unwrap();
assert!(
loaded
.diagnostics
.iter()
.any(|d| d.diagnostic.code == "invalid_path")
);
assert!(!loaded.bundle.files.contains_key("alias.arcadia"));
fs::remove_file(&link).unwrap();
}
}
#[test]
fn forged_compiled_model_or_index_cannot_be_saved() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
let snapshot = load_project(&entry, ProjectLimits::default()).unwrap();
let mut compiled =
compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
compiled.decoded.project.name = "forged".into();
assert!(matches!(
save_project(&snapshot, &compiled, ProjectLimits::default()),
Err(SourceFileError::Invalid(_))
));
assert_eq!(
fs::read(&entry).unwrap(),
snapshot.bundle.files["project.arcadia"].as_bytes()
);
}
#[cfg(unix)]
#[test]
fn named_pipes_are_rejected_before_opening_on_load_discovery_and_save() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
let child = directory.path().join("parts/a.arcadia");
let snapshot = load_project(&entry, ProjectLimits::default()).unwrap();
let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
fs::remove_file(&child).unwrap();
assert!(
std::process::Command::new("mkfifo")
.arg(&child)
.status()
.unwrap()
.success()
);
let loaded = load_project(&entry, ProjectLimits::default()).unwrap();
assert!(
loaded
.diagnostics
.iter()
.any(|d| d.diagnostic.code == "invalid_path"
&& d.diagnostic.message.contains("not a regular file"))
);
assert!(!loaded.bundle.files.contains_key("parts/a.arcadia"));
assert!(matches!(
discover_missing(&snapshot, &BTreeMap::new(), ProjectLimits::default()),
Err(SourceFileError::Path(_))
));
assert!(matches!(
save_project(&snapshot, &compiled, ProjectLimits::default()),
Err(SourceFileError::Path(_))
));
assert_eq!(
fs::read_to_string(&entry).unwrap(),
snapshot.bundle.files["project.arcadia"]
);
}
#[cfg(unix)]
#[test]
fn atomic_save_preserves_existing_unix_permissions() {
use std::os::unix::fs::PermissionsExt;
let directory = setup();
let entry = directory.path().join("project.arcadia");
let child = directory.path().join("parts/a.arcadia");
fs::set_permissions(&entry, fs::Permissions::from_mode(0o664)).unwrap();
fs::set_permissions(&child, fs::Permissions::from_mode(0o640)).unwrap();
let snapshot = load_project(&entry, ProjectLimits::default()).unwrap();
let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap();
assert_eq!(
fs::metadata(&entry).unwrap().permissions().mode() & 0o777,
0o664
);
assert_eq!(
fs::metadata(&child).unwrap().permissions().mode() & 0o777,
0o640
);
}
#[test]
fn discovery_diagnostics_keep_raw_no_uuid_spans() {
let directory = setup();
let path = directory.path().join("parts/a.arcadia");
let raw =
"// Unicode: 航海\nsa_function first \"First\" {}\nsa_function second \"Second\" {}\n";
fs::write(&path, raw).unwrap();
let disk = load_project(
&directory.path().join("project.arcadia"),
ProjectLimits::default(),
)
.unwrap();
assert_eq!(disk.bundle.files["parts/a.arcadia"], raw);
let issue = disk
.diagnostics
.iter()
.find(|issue| {
issue.diagnostic.code == "unallocated_leaf"
&& issue.diagnostic.message.contains("Second")
})
.unwrap();
assert_eq!(issue.path, "parts/a.arcadia");
assert_eq!(issue.diagnostic.line, 3);
assert_eq!(issue.diagnostic.column, 1);
let span = issue.diagnostic.span;
assert_eq!(span.start, raw.find("sa_function second").unwrap());
assert_eq!(
&raw[span.start..span.end],
"sa_function second \"Second\" {}"
);
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()).unwrap();
assert_ne!(compiled.bundle, disk.bundle);
assert!(compiled.diagnostics.iter().any(|later| {
later.diagnostic.code == issue.diagnostic.code
&& later.diagnostic.message == issue.diagnostic.message
&& later.diagnostic.span.start > span.start
}));
assert_eq!(fs::read_to_string(path).unwrap(), raw);
}
#[test]
fn discovery_diagnoses_legacy_semantics_before_source_rewrite() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("project.arcadia");
let raw = include_str!("fixtures/v3-data-contract.arcadia");
fs::write(&path, raw).unwrap();
let disk = load_project(&path, ProjectLimits::default()).unwrap();
assert_eq!(disk.bundle.files["project.arcadia"], raw);
assert!(!disk.diagnostics.is_empty());
assert!(
disk.diagnostics
.iter()
.all(|issue| { issue.diagnostic.severity == oa_core::validation::Severity::Warning })
);
assert_eq!(
disk.diagnostics,
lint_bundle(&disk.reachable_bundle(), ProjectLimits::default())
);
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.migration.as_ref().unwrap().from_schema, 3);
assert_ne!(compiled.bundle, disk.bundle);
for issue in &disk.diagnostics {
assert!(compiled.diagnostics.iter().any(|later| {
later.diagnostic.code == issue.diagnostic.code
&& later.diagnostic.object_id == issue.diagnostic.object_id
&& later.diagnostic.message == issue.diagnostic.message
}));
let span = issue.diagnostic.span;
assert!(raw.get(span.start..span.end).is_some());
}
// Invalid legacy references still fail during semantic resolution on the
// authored bytes; deferring the source rewrite does not hide those errors.
let invalid = raw.replace("@coordinate", "@missing");
fs::write(&path, &invalid).unwrap();
let broken = load_project(&path, ProjectLimits::default()).unwrap();
assert!(broken.diagnostics.iter().any(|issue| {
issue.diagnostic.severity == oa_core::validation::Severity::Error
&& issue.diagnostic.message.contains("@missing")
}));
assert_eq!(broken.bundle.files["project.arcadia"], invalid);
assert!(compile_bundle(&broken.reachable_bundle(), ProjectLimits::default()).is_err());
assert_eq!(fs::read_to_string(path).unwrap(), invalid);
}