use super::*; const PROJECT: &str = "00000000-0000-0000-0000-000000000001"; const PUMP: &str = "00000000-0000-0000-0000-000000000002"; const LINK: &str = "00000000-0000-0000-0000-000000000003"; #[derive(Clone)] struct Fixture { entry: String, files: Vec<(String, String)>, } impl Fixture { fn standard() -> Self { Self { entry: "project.arcadia".into(), files: vec![ ( "project.arcadia".into(), format!( "arcadia 7;\r\nproject model \"Harbour\" id \"{PROJECT}\" {{\r\n include \"模块/活動.arcadia\";\r\n include \"links.arcadia\";\r\n}}\r\n" ), ), ( "模块/活動.arcadia".into(), format!( "// 原文 remains exact\r\noa_activity pump \"泵 🚀\" id \"{PUMP}\" {{}}\r\n" ), ), ( "links.arcadia".into(), format!( "// 航海\r\ninteraction connection \"流れ\" id \"{LINK}\" {{\r\n source @ // separator @pump\r\n pump;\r\n description \"船 @pump\"; target @pump;\r\n}}\r\n" ), ), ], } } fn borrowed(&self) -> Vec> { self.files .iter() .map(|(path, text)| DraftFile { path, text }) .collect() } fn text(&self, path: &str) -> &str { &self .files .iter() .find(|(candidate, _)| candidate == path) .unwrap() .1 } fn text_mut(&mut self, path: &str) -> &mut String { &mut self .files .iter_mut() .find(|(candidate, _)| candidate == path) .unwrap() .1 } fn snapshot(&self, validated: Option<&Self>, applied: bool, limits: Limits) -> Snapshot { let current = self.borrowed(); let old = validated.map(Self::borrowed); capture( Input { key: key(1), entry: &self.entry, files: ¤t, applied, validation: old.as_ref().map(|files| ValidatedSources { entry: &validated.unwrap().entry, files, }), }, limits, ) .unwrap() } } fn key(revision: u64) -> SnapshotKey { SnapshotKey { workspace: Id::from_u128(100), revision, } } fn query(snapshot: &Snapshot, path: &str, byte_offset: usize, action: Action) -> Results { snapshot .query(Query { stamp: QueryStamp { snapshot: snapshot.key(), serial: 37, }, path, byte_offset, action, }) .unwrap() } fn reference_offset(fixture: &Fixture) -> usize { fixture.text("links.arcadia").find("source @").unwrap() + "source ".len() } #[test] fn exact_multifile_unicode_crlf_definition_and_repeated_lexical_references() { let fixture = Fixture::standard(); let original = fixture.files.clone(); let snapshot = fixture.snapshot(Some(&fixture), true, Limits::default()); assert!(snapshot.scope().closure_complete && snapshot.scope().validated); assert_eq!( ( snapshot.scope().included_files, snapshot.scope().detached_files ), (3, 0) ); let result = query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::Definition, ); assert_eq!(result.disposition, Disposition::Direct); assert_eq!( (result.total_definitions, result.omitted_definitions), (1, 0) ); let definition = &result.definitions[0]; assert_eq!(definition.path, "模块/活動.arcadia"); assert_eq!(definition.declaration.kind, "oa_activity"); assert_eq!(definition.declaration.name.as_deref(), Some("泵 🚀")); assert_eq!( definition.declaration.authored_id, Some(Id::parse_str(PUMP).unwrap()) ); assert_eq!( definition.position, Position { line: 1, byte_column: "oa_activity ".len() } ); assert_eq!( &fixture.text(&definition.path)[definition.span.start..definition.span.end], "pump" ); let uses = query( &snapshot, &definition.path, definition.span.start, Action::References, ); assert_eq!(uses.disposition, Disposition::LexicalReferences); assert_eq!((uses.total_references, uses.omitted_references), (2, 0)); assert_eq!( uses.references[0].position, Position { line: 3, byte_column: 3 } ); let target_prefix = " description \"船 @pump\"; target @"; assert_ne!(target_prefix.len(), target_prefix.chars().count()); assert_eq!( uses.references[1].position, Position { line: 4, byte_column: target_prefix.len() } ); for row in &uses.references { let text = fixture.text(&row.path); assert_eq!(&text[row.span.start..row.span.end], "pump"); assert_eq!(&text[row.marker.start..row.marker.end], "@"); let context = row.context.as_ref().unwrap(); assert_eq!(context.alias, "connection"); assert_eq!(context.kind, "interaction"); assert_eq!(context.name.as_deref(), Some("流れ")); assert_eq!(row.evidence, Evidence::Validated); } assert_eq!( uses.stamp, QueryStamp { snapshot: key(1), serial: 37 } ); assert_eq!( fixture.files, original, "no stabilization, newline rewrite or accepted adoption" ); for (index, (path, text)) in original.iter().enumerate() { let owned = snapshot.file(index).unwrap(); assert_eq!((owned.path, owned.text), (path.as_str(), text.as_str())); } } #[test] fn comments_strings_and_comment_gap_are_not_navigation_hits() { let fixture = Fixture::standard(); let snapshot = fixture.snapshot(None, true, Limits::default()); let text = fixture.text("links.arcadia"); for offset in [ text.find("separator @pump").unwrap() + "separator ".len(), text.find("船 @pump").unwrap() + "船 ".len(), text.find("@ //").unwrap() + 1, ] { let result = query(&snapshot, "links.arcadia", offset, Action::Definition); assert_eq!(result.disposition, Disposition::NoSymbol); assert_eq!(result.total_definitions, 0); } assert_eq!( query( &snapshot, "links.arcadia", text.find(" pump;").unwrap() + 3, Action::Definition ) .total_definitions, 1 ); let inside_utf8 = text.find('船').unwrap() + 1; assert_eq!( snapshot.query(Query { stamp: QueryStamp { snapshot: key(1), serial: 1 }, path: "links.arcadia", byte_offset: inside_utf8, action: Action::Definition }), Err(QueryError::Query(symbols::QueryError::InvalidBytePosition)) ); } #[test] fn dirty_but_parseable_text_has_current_names_and_no_validated_fallback() { let original = Fixture::standard(); let mut dirty = original.clone(); *dirty.text_mut("模块/活動.arcadia") = dirty .text("模块/活動.arcadia") .replace("泵 🚀", "Edited 泵\\nwith detail"); let snapshot = dirty.snapshot(Some(&original), true, Limits::default()); assert!(snapshot.scope().closure_complete); assert!(!snapshot.scope().validated); let result = query( &snapshot, "links.arcadia", reference_offset(&dirty), Action::Definition, ); assert_eq!(result.disposition, Disposition::Choices); assert_eq!( result.definitions[0].declaration.name.as_deref(), Some("Edited 泵\nwith detail") ); assert_eq!(result.definitions[0].evidence, Evidence::Draft); assert_eq!( snapshot.file(1).unwrap().text, dirty.text("模块/活動.arcadia") ); assert_ne!( snapshot.file(1).unwrap().text, original.text("模块/活動.arcadia") ); } #[test] fn invalid_include_syntax_never_borrows_the_old_reachable_file_set() { let original = Fixture::standard(); let mut invalid = original.clone(); *invalid.text_mut("project.arcadia") = invalid .text("project.arcadia") .replace("include \"links.arcadia\";", "include \"links.arcadia\""); let snapshot = invalid.snapshot(Some(&original), true, Limits::default()); assert!(snapshot.scope().partial()); assert!(!snapshot.scope().validated); assert!( snapshot .scope() .issues .iter() .any(|issue| issue.kind == ScopeIssueKind::InvalidIncludes) ); let result = query( &snapshot, "links.arcadia", reference_offset(&invalid), Action::Definition, ); assert_eq!(result.total_definitions, 1); assert_eq!(result.disposition, Disposition::Choices); assert_eq!(result.definitions[0].membership, Membership::Unverified); } #[test] fn missing_include_partial_scope_does_not_resurrect_witness_text() { let original = Fixture::standard(); let mut missing = original.clone(); missing .files .retain(|(path, _)| path != "模块/活動.arcadia"); let snapshot = missing.snapshot(Some(&original), true, Limits::default()); assert!(snapshot.scope().partial()); assert!( snapshot .scope() .issues .iter() .any(|issue| issue.kind == ScopeIssueKind::MissingInclude) ); let result = query( &snapshot, "links.arcadia", reference_offset(&missing), Action::Definition, ); assert_eq!(result.disposition, Disposition::NoDefinition); assert!(result.definitions.is_empty()); assert!( result.scope.partial(), "zero means no loaded definition, not no project definition" ); assert!(snapshot.file(2).is_none()); } #[test] fn detached_alias_and_identity_collisions_never_auto_select() { let accepted = Fixture::standard(); for (alias, id) in [ ("pump", "00000000-0000-0000-0000-000000000099"), ("other", PUMP), ] { let mut loaded = accepted.clone(); loaded.files.push(( "detached.arcadia".into(), format!("oa_activity {alias} \"Detached\" id \"{id}\" {{}}"), )); let snapshot = loaded.snapshot(Some(&accepted), true, Limits::default()); assert!(snapshot.scope().validated); assert_eq!(snapshot.scope().detached_files, 1); let result = query( &snapshot, "links.arcadia", reference_offset(&loaded), Action::Definition, ); assert_eq!(result.disposition, Disposition::Choices); if alias == "pump" { assert_eq!(result.total_definitions, 2); assert!(result.definitions.iter().any( |row| row.membership == Membership::Detached && row.evidence == Evidence::Draft )); } else { assert_eq!(result.total_definitions, 1); assert_eq!( result.definitions[0].declaration.identity, Identity::Collision { count: 2 } ); } } } #[test] fn sole_detached_candidate_and_unapplied_equal_bytes_require_choice() { let accepted = Fixture::standard(); let mut detached = accepted.clone(); *detached.text_mut("project.arcadia") = detached .text("project.arcadia") .replace(" include \"模块/活動.arcadia\";\r\n", ""); let snapshot = detached.snapshot(None, true, Limits::default()); let result = query( &snapshot, "links.arcadia", reference_offset(&detached), Action::Definition, ); assert_eq!(result.disposition, Disposition::Choices); assert_eq!(result.definitions[0].membership, Membership::Detached); let unapplied = accepted.snapshot(Some(&accepted), false, Limits::default()); assert!(unapplied.scope().closure_complete && unapplied.scope().partial()); assert!(!unapplied.scope().validated); assert_eq!( query( &unapplied, "links.arcadia", reference_offset(&accepted), Action::Definition ) .disposition, Disposition::Choices ); } #[test] fn include_cycle_depth_and_aggregate_edges_are_explicit_partial_outcomes() { let mut fixture = Fixture::standard(); fixture .text_mut("模块/活動.arcadia") .push_str("include \"../project.arcadia\";\r\n"); let cycle = fixture.snapshot(None, true, Limits::default()); assert!( cycle .scope() .issues .iter() .any(|issue| issue.kind == ScopeIssueKind::IncludeCycle) ); let fixture = Fixture::standard(); for (depth, edges, expected) in [ (0, 1024, ScopeIssueKind::IncludeDepth), (32, 1, ScopeIssueKind::IncludeBudget), ] { let snapshot = fixture.snapshot( None, true, Limits { include_depth: depth, include_edges: edges, ..Limits::default() }, ); assert!(snapshot.scope().partial()); assert!( snapshot .scope() .issues .iter() .any(|issue| issue.kind == expected) ); assert_eq!( query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::Definition ) .disposition, Disposition::Choices ); } } #[test] fn exact_snapshot_readback_rejects_same_key_byte_membership_and_evidence_changes() { let fixture = Fixture::standard(); let files = fixture.borrowed(); let validation = Some(ValidatedSources { entry: &fixture.entry, files: &files, }); let input = Input { key: key(1), entry: &fixture.entry, files: &files, applied: true, validation, }; let snapshot = capture(input, Limits::default()).unwrap(); assert!(snapshot.matches(input)); assert!(!snapshot.matches(Input { key: key(2), ..input })); assert!(!snapshot.matches(Input { validation: None, ..input })); assert!(!snapshot.matches(Input { applied: false, ..input })); let mut reversed = files.clone(); reversed.reverse(); assert!(snapshot.matches(Input { files: &reversed, ..input })); let mut changed = fixture.clone(); changed.text_mut("links.arcadia").push(' '); let altered = changed.borrowed(); assert!(!snapshot.matches(Input { files: &altered, ..input })); let missing = &files[..files.len() - 1]; assert!(!snapshot.matches(Input { files: missing, ..input })); let duplicated = [files[0], files[0], files[2]]; assert!(!snapshot.matches(Input { files: &duplicated, ..input })); assert_eq!( snapshot.query(Query { stamp: QueryStamp { snapshot: key(2), serial: 37 }, path: "links.arcadia", byte_offset: 0, action: Action::Definition }), Err(QueryError::Query(symbols::QueryError::StaleSnapshot)) ); } #[test] fn omitted_rows_are_counted_before_result_allocation_and_never_confer_uniqueness() { let fixture = Fixture::standard(); let snapshot = fixture.snapshot( Some(&fixture), true, Limits { result_rows: 2, ..Limits::default() }, ); let result = query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::References, ); assert_eq!((result.total_definitions, result.total_references), (1, 2)); assert_eq!( ( result.definitions.len(), result.references.len(), result.omitted_references ), (1, 1, 1) ); let zero = fixture.snapshot( Some(&fixture), true, Limits { result_rows: 0, ..Limits::default() }, ); let result = query( &zero, "links.arcadia", reference_offset(&fixture), Action::Definition, ); assert_eq!( ( result.definitions.len(), result.total_definitions, result.omitted_definitions ), (0, 1, 1) ); assert_eq!(result.disposition, Disposition::Choices); let envelope = fixture.snapshot( Some(&fixture), true, Limits { result_bytes: 0, ..Limits::default() }, ); assert_eq!( envelope.query(Query { stamp: QueryStamp { snapshot: key(1), serial: 1 }, path: "links.arcadia", byte_offset: reference_offset(&fixture), action: Action::Definition }), Err(QueryError::ResultEnvelopeLimit) ); } #[test] fn large_display_name_cannot_escape_the_owned_row_string_budget() { let mut fixture = Fixture::standard(); *fixture.text_mut("模块/活動.arcadia") = fixture .text("模块/活動.arcadia") .replace("泵 🚀", &"長".repeat(8192)); let original = fixture.files.clone(); let snapshot = fixture.snapshot( None, true, Limits { result_bytes: 64, ..Limits::default() }, ); let result = query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::Definition, ); assert_eq!( (result.total_definitions, result.omitted_definitions), (1, 1) ); assert!(result.definitions.is_empty()); assert_eq!(result.disposition, Disposition::Choices); assert_eq!(fixture.files, original); } #[test] fn validation_witness_must_match_entry_and_complete_included_file_set() { let fixture = Fixture::standard(); for mutation in 0..3 { let mut witness = fixture.clone(); match mutation { 0 => witness.entry = "other.arcadia".into(), 1 => { witness.files.pop(); } _ => witness.files.push(("unused.arcadia".into(), String::new())), } let snapshot = fixture.snapshot(Some(&witness), true, Limits::default()); assert!(snapshot.scope().closure_complete); assert!(!snapshot.scope().validated); assert_eq!( query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::Definition ) .disposition, Disposition::Choices ); } } #[test] fn raw_aggregate_limits_refuse_before_outline_or_copied_snapshot() { // Token budget zero and malformed paths/text would fail later. Raw byte/path // limits must win, including a large second file; no partial Snapshot exists. let large = "not a model \0".repeat(16_384); let files = [ DraftFile { path: "../bad.arcadia", text: "?", }, DraftFile { path: "large.arcadia", text: &large, }, ]; let input = Input { key: key(1), entry: "project.arcadia", files: &files, applied: true, validation: None, }; for (resource, files_limit, bytes_limit, paths_limit) in [ (symbols::Resource::Files, 1, usize::MAX, usize::MAX), (symbols::Resource::SourceBytes, 8, 1, usize::MAX), (symbols::Resource::PathBytes, 8, usize::MAX, 1), ] { let limits = Limits { index: symbols::Limits { files: files_limit, source_bytes: bytes_limit, path_bytes: paths_limit, tokens: 0, records: 0, }, ..Limits::default() }; assert!( matches!(capture(input, limits), Err(AdmissionError::Index(symbols::BuildError::Limit { resource: actual, .. })) if actual == resource) ); } assert_eq!(files[1].text, large); } #[test] fn token_declaration_and_live_reference_limits_are_aggregate_not_per_file() { let files = [ DraftFile { path: "a.arcadia", text: "oa_activity a \"A\" {}", }, DraftFile { path: "b.arcadia", text: "oa_activity b \"B\" {}", }, ]; let input = Input { key: key(1), entry: "a.arcadia", files: &files, applied: false, validation: None, }; let limits = Limits { index: symbols::Limits { tokens: 10, ..Limits::default().index }, ..Limits::default() }; assert!(matches!( capture(input, limits), Err(AdmissionError::Outline { file: 1, error: language::OutlineLimit { resource: language::OutlineResource::Tokens, .. } }) )); let limits = Limits { index: symbols::Limits { records: 1, ..Limits::default().index }, ..Limits::default() }; assert!(matches!( capture(input, limits), Err(AdmissionError::Outline { file: 1, error: language::OutlineLimit { resource: language::OutlineResource::Declarations, .. } }) )); let use_files = [ files[0], DraftFile { path: "uses.arcadia", text: "source @a; target @a; // @a ignored\n", }, ]; let limits = Limits { index: symbols::Limits { records: 2, ..Limits::default().index }, ..Limits::default() }; assert!(matches!( capture( Input { files: &use_files, ..input }, limits ), Err(AdmissionError::Index(symbols::BuildError::Limit { resource: symbols::Resource::Records, observed_at_least: 3, .. })) )); } #[test] fn missing_and_noncanonical_authored_ids_remain_draft_spans_without_generated_anchors() { let fixture = Fixture::standard(); for replacement in [ String::new(), " id \"00000000000000000000000000000002\"".into(), ] { let mut draft = fixture.clone(); let original_id = format!(" id \"{PUMP}\""); *draft.text_mut("模块/活動.arcadia") = draft .text("模块/活動.arcadia") .replace(&original_id, &replacement); draft .text_mut("模块/活動.arcadia") .push_str(&format!("// Historical UUID {PUMP} and @pump\r\n")); let before = draft.files.clone(); let snapshot = draft.snapshot(None, false, Limits::default()); let result = query( &snapshot, "links.arcadia", reference_offset(&draft), Action::Definition, ); assert_eq!(result.definitions[0].declaration.authored_id, None); assert_eq!( result.definitions[0].declaration.identity, Identity::Missing ); assert_eq!(result.disposition, Disposition::Choices); assert_eq!(draft.files, before); assert_eq!( snapshot.file(1).unwrap().text, draft.text("模块/活動.arcadia") ); } } #[test] fn keyword_alias_and_loaded_path_case_collisions_use_reviewed_index_rules() { let mut fixture = Fixture::standard(); for (_, text) in &mut fixture.files { *text = text.replace("pump", "source"); } let snapshot = fixture.snapshot(None, true, Limits::default()); let result = query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::Definition, ); assert_eq!(result.alias.as_deref(), Some("source")); assert_eq!(result.total_definitions, 1); fixture.files.push(("LINKS.arcadia".into(), String::new())); let files = fixture.borrowed(); assert!(matches!( capture( Input { key: key(1), entry: &fixture.entry, files: &files, applied: true, validation: None }, Limits::default() ), Err(AdmissionError::Index(symbols::BuildError::Input { problem: symbols::InputProblem::DuplicateOrCaseCollidingPath, .. })) )); } #[test] fn owned_request_moves_exact_text_into_worker_snapshot_and_matches_sync_results() { let fixture = Fixture::standard(); let files = fixture.borrowed(); let input = Input { key: key(1), entry: &fixture.entry, files: &files, applied: true, validation: Some(ValidatedSources { entry: &fixture.entry, files: &files, }), }; let owned = admit_owned(input, Limits::default()).unwrap(); let allocations: Vec<_> = owned.files.iter().map(|file| file.text.as_ptr()).collect(); let snapshot = owned.capture().unwrap(); for (file, allocation) in allocations.iter().enumerate() { assert_eq!( snapshot.file(file).unwrap().text.as_ptr(), *allocation, "worker moves the one current-text copy" ); } assert!(snapshot.matches(input)); let synchronous = capture(input, Limits::default()).unwrap(); assert_eq!( query( &snapshot, "links.arcadia", reference_offset(&fixture), Action::References ), query( &synchronous, "links.arcadia", reference_offset(&fixture), Action::References ) ); let mut edited = fixture.clone(); edited .text_mut("links.arcadia") .push_str("// edited after request\r\n"); let changed = edited.borrowed(); assert!(!snapshot.matches(Input { files: &changed, ..input })); assert!(!snapshot.matches(Input { validation: None, ..input })); } #[test] fn owned_request_checks_raw_limits_immediately_and_extracts_only_on_worker() { let fixture = Fixture::standard(); let files = fixture.borrowed(); let input = Input { key: key(1), entry: &fixture.entry, files: &files, applied: true, validation: None, }; let no_tokens = Limits { index: symbols::Limits { tokens: 0, ..Limits::default().index }, ..Limits::default() }; let owned = admit_owned(input, no_tokens).expect("request admission does not lex or parse"); assert!(matches!( owned.capture(), Err(AdmissionError::Outline { file: 0, error: language::OutlineLimit { resource: language::OutlineResource::Tokens, .. } }) )); let no_bytes = Limits { index: symbols::Limits { source_bytes: 1, ..Limits::default().index }, ..Limits::default() }; assert!(matches!( admit_owned(input, no_bytes), Err(AdmissionError::Index(symbols::BuildError::Limit { resource: symbols::Resource::SourceBytes, .. })) )); assert_eq!(files[1].text, fixture.text("模块/活動.arcadia")); } #[test] fn owned_validation_proof_preserves_full_witness_count_and_partial_scope() { let accepted = Fixture::standard(); for missing_include in [false, true] { let mut current = accepted.clone(); current .files .retain(|(path, _)| path != "模块/活動.arcadia"); if !missing_include { *current.text_mut("project.arcadia") = current .text("project.arcadia") .replace(" include \"模块/活動.arcadia\";\r\n", ""); } let files = current.borrowed(); let old = accepted.borrowed(); let input = Input { key: key(1), entry: ¤t.entry, files: &files, applied: true, validation: Some(ValidatedSources { entry: &accepted.entry, files: &old, }), }; let snapshot = admit_owned(input, Limits::default()) .unwrap() .capture() .unwrap(); assert_eq!(snapshot.scope().closure_complete, !missing_include); assert!( !snapshot.scope().validated, "the old witness is not silently reduced to matching loaded files" ); let result = query( &snapshot, "links.arcadia", reference_offset(¤t), Action::Definition, ); assert_eq!(result.disposition, Disposition::NoDefinition); assert!(snapshot.matches(input)); } }