Files
ARCADIA-Studio/crates/oa-core/tests/coverage_diagnostics.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
2026-09-10 10:06:55 -07:00

586 lines
20 KiB
Rust

use oa_core::{
language,
source_project::*,
validation::{self, Severity},
*,
};
use std::collections::{BTreeMap, BTreeSet};
const CODES: [&str; 5] = [
"non_leaf_work_exchange",
"work_without_behavior_context",
"exchange_without_behavior_context",
"mission_without_capability",
"capability_without_behavior_context",
];
type Located = (String, Id, Option<String>);
fn coverage(p: &Project) -> BTreeSet<Located> {
let issues = validation::validate(p);
assert!(
!issues.iter().any(|i| i.severity == Severity::Error),
"{issues:?}"
);
issues
.into_iter()
.filter(|i| CODES.contains(&i.code.as_str()))
.map(|i| {
assert_eq!(i.severity, Severity::Warning);
(i.code, i.element.unwrap(), i.field)
})
.collect()
}
fn located(code: &str, id: Id, field: &str) -> Located {
(code.into(), id, (!field.is_empty()).then(|| field.into()))
}
fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
p.add_element(kind, layer, name, None).unwrap()
}
fn occurrence(p: &mut Project, owner: Id, body: OccurrenceBody) -> Id {
let id = p
.add_element(
Kind::Occurrence,
p.elements[&owner].layer(),
"Contribution",
Some(owner),
)
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body,
precondition: String::new(),
postcondition: String::new(),
};
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() {
steps.push(id);
}
id
}
fn exchange(id: Id) -> OccurrenceBody {
OccurrenceBody::Exchange {
operation: None,
exchange: Some(id),
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
reply_to: None,
values: vec![],
}
}
fn action(id: Id) -> OccurrenceBody {
OccurrenceBody::Action {
action: Some(id),
lifeline: None,
}
}
fn line(p: &mut Project, scenario: Id, work: Id) -> Id {
let id = p
.add_element(
Kind::Lifeline,
p.elements[&scenario].layer(),
"Work line",
Some(scenario),
)
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(work),
};
id
}
fn set_context(p: &mut Project, scenario: Id, capability: Id) {
if let Some(SharedBody::Scenario { context, .. }) =
p.elements.get_mut(&scenario).unwrap().shared_mut()
{
*context = Some(capability);
}
}
#[test]
fn included_process_and_nested_scenario_evidence_is_identity_based_in_both_layers() {
for layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let (
work_kind,
sequence_kind,
capability_kind,
mission_kind,
performer_kind,
exchange_kind,
) = if layer == Layer::Oa {
(
Kind::Activity,
Kind::Process,
Kind::Capability,
Kind::OperationalMission,
Kind::Entity,
RelationKind::Interaction,
)
} else {
(
Kind::Function,
Kind::FunctionalChain,
Kind::SystemCapability,
Kind::Mission,
Kind::System,
RelationKind::FunctionalExchange,
)
};
let performer = add(&mut p, performer_kind, layer, "Service provider");
let a = add(&mut p, work_kind, layer, "Request service");
let b = add(&mut p, work_kind, layer, "Provide service");
for work in [a, b] {
p.relate(RelationKind::Allocation, "Performed work", performer, work);
}
let e = p.relate(exchange_kind, "Request", a, b);
let inner = add(&mut p, sequence_kind, layer, "Service path");
let outer = add(&mut p, sequence_kind, layer, "Reusable path");
occurrence(&mut p, inner, exchange(e));
occurrence(
&mut p,
outer,
OccurrenceBody::SequenceReference {
sequence: Some(inner),
},
);
let capability = add(&mut p, capability_kind, layer, "Deliver service");
p.relate(
RelationKind::ChainInvolvement,
"Service context",
outer,
capability,
);
let mission = add(&mut p, mission_kind, layer, "Serve users");
p.relate(
RelationKind::MissionCapability,
"Delivered capability",
mission,
capability,
);
let scenario = add(&mut p, Kind::Scenario, layer, "Conversation");
let sender = line(&mut p, scenario, a);
let receiver = line(&mut p, scenario, b);
let fragment = occurrence(
&mut p,
scenario,
OccurrenceBody::Fragment {
operator: FragmentOperator::Optional,
branches: vec![],
},
);
let branch = occurrence(
&mut p,
fragment,
OccurrenceBody::Branch {
guard: "service_requested".into(),
occurrences: vec![],
},
);
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
{
branches.push(branch);
}
let call = occurrence(
&mut p,
branch,
OccurrenceBody::Exchange {
operation: None,
exchange: Some(e),
sender: Some(sender),
receiver: Some(receiver),
message_kind: MessageKind::Call,
reply_to: None,
values: vec![],
},
);
occurrence(
&mut p,
branch,
OccurrenceBody::Exchange {
operation: None,
exchange: Some(e),
sender: Some(receiver),
receiver: Some(sender),
message_kind: MessageKind::Reply,
reply_to: Some(call),
values: vec![],
},
);
let reuse = add(&mut p, Kind::Scenario, layer, "Reused conversation");
let outer_sender = line(&mut p, reuse, a);
let outer_receiver = line(&mut p, reuse, b);
occurrence(
&mut p,
reuse,
OccurrenceBody::ScenarioUse {
scenario: Some(scenario),
bindings: vec![
LifelineBinding {
referenced: sender,
local: outer_sender,
},
LifelineBinding {
referenced: receiver,
local: outer_receiver,
},
],
},
);
assert!(coverage(&p).is_empty());
let inner_steps = std::mem::take(p.elements.get_mut(&inner).unwrap().steps_mut().unwrap());
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.clear();
assert_eq!(
coverage(&p),
BTreeSet::from([
located("work_without_behavior_context", a, ""),
located("work_without_behavior_context", b, ""),
located("exchange_without_behavior_context", e, ""),
]),
"Unordered fragments and reused containers cannot activate omitted messages"
);
*p.elements.get_mut(&inner).unwrap().steps_mut().unwrap() = inner_steps;
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(fragment);
assert!(coverage(&p).is_empty());
// Removing every message leaves lifelines and reuse intact: neither is
// an execution claim. Empty sequences remain saveable, with old warnings.
for element in p.elements.values_mut() {
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { .. },
..
}) = element.shared()
{
*element.shared_mut().unwrap() = SharedBody::Occurrence {
body: OccurrenceBody::Action {
action: None,
lifeline: None,
},
precondition: String::new(),
postcondition: String::new(),
};
}
}
assert_eq!(
coverage(&p),
BTreeSet::from([
located("work_without_behavior_context", a, ""),
located("work_without_behavior_context", b, ""),
located("exchange_without_behavior_context", e, "")
])
);
occurrence(&mut p, scenario, action(a)); // known work, unfinished lifeline
assert_eq!(
coverage(&p),
BTreeSet::from([
located("work_without_behavior_context", b, ""),
located("exchange_without_behavior_context", e, "")
])
);
assert_eq!(language::parse(&language::format(&p)).unwrap(), p);
}
}
#[test]
fn incomplete_oa_repairs_exact_records_and_ordered_membership_without_inference() {
let mut p = Project::empty();
let performer = add(&mut p, Kind::Entity, Layer::Oa, "Crew");
let parent = add(&mut p, Kind::Activity, Layer::Oa, "Manage service");
let a = p
.add_element(Kind::Activity, Layer::Oa, "Prepare", Some(parent))
.unwrap();
let b = p
.add_element(Kind::Activity, Layer::Oa, "Deliver", Some(parent))
.unwrap();
let c = add(&mut p, Kind::Activity, Layer::Oa, "Verify");
for work in [a, b, c] {
p.relate(RelationKind::Allocation, "Responsibility", performer, work);
}
let ab = p.relate(RelationKind::Interaction, "Prepared service", a, b);
let pc = p.relate(RelationKind::Interaction, "Grouped service", parent, c);
let ca = p.relate(RelationKind::Interaction, "Verified service", c, a);
let process = add(&mut p, Kind::Process, Layer::Oa, "Delivery path");
occurrence(&mut p, process, exchange(ab));
let mission = add(
&mut p,
Kind::OperationalMission,
Layer::Oa,
"Deliver reliably",
);
let capability = add(&mut p, Kind::Capability, Layer::Oa, "Reliable delivery");
p.relate(
RelationKind::CapabilityInvolvement,
"Crew support",
performer,
capability,
);
p.relate(
RelationKind::MissionInvolvement,
"Mission responsibility",
performer,
mission,
);
let machine = p
.add_element(Kind::StateMachine, Layer::Oa, "Crew modes", Some(performer))
.unwrap();
let region = p
.add_element(Kind::Region, Layer::Oa, "Crew region", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, Layer::Oa, "Available", Some(region))
.unwrap();
p.relate(RelationKind::StateAction, "Verification entry", state, c);
let observation = add(&mut p, Kind::Scenario, Layer::Oa, "Observation only");
let observed = line(&mut p, observation, c);
occurrence(
&mut p,
observation,
OccurrenceBody::State {
state: Some(state),
lifeline: Some(observed),
},
);
let expected = BTreeSet::from([
located("non_leaf_work_exchange", pc, "source"),
located("work_without_behavior_context", c, ""),
located("exchange_without_behavior_context", ca, ""),
located("mission_without_capability", mission, ""),
located("capability_without_behavior_context", capability, ""),
]);
assert_eq!(coverage(&p), expected);
let unordered = occurrence(&mut p, process, exchange(ca));
p.elements
.get_mut(&process)
.unwrap()
.steps_mut()
.unwrap()
.retain(|id| *id != unordered);
assert_eq!(coverage(&p), expected);
assert!(
validation::validate(&p)
.iter()
.any(|i| i.element == Some(unordered) && i.code == "unordered_occurrence")
);
let before = p.clone();
p.elements
.get_mut(&process)
.unwrap()
.steps_mut()
.unwrap()
.push(unordered);
if let RelationBody::Interaction { source, .. } = &mut p.relations.get_mut(&pc).unwrap().body {
*source = a;
}
occurrence(&mut p, process, exchange(pc));
p.relate(
RelationKind::MissionCapability,
"Mission delivery",
mission,
capability,
);
p.relate(
RelationKind::ChainInvolvement,
"Capability path",
process,
capability,
);
assert!(coverage(&p).is_empty());
// Public session admission and undo retain the previous exact warning set.
let mut session = Session::new(before.clone());
session
.execute(Command::Batch(diff_commands(&before, &p)))
.unwrap();
assert!(coverage(session.project()).is_empty());
session.undo();
assert_eq!(session.project(), &before);
assert_eq!(coverage(session.project()), expected);
}
#[test]
fn non_leaf_exchange_locations_use_work_endpoints_including_typed_sa_ports() {
for layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let (kind, relation_kind) = if layer == Layer::Oa {
(Kind::Activity, RelationKind::Interaction)
} else {
(Kind::Function, RelationKind::FunctionalExchange)
};
let source = add(&mut p, kind, layer, "Source group");
let target = add(&mut p, kind, layer, "Target group");
let a = p
.add_element(kind, layer, "Source leaf", Some(source))
.unwrap();
let b = p
.add_element(kind, layer, "Target leaf", Some(target))
.unwrap();
let edge = p.relate(relation_kind, "Decompose exchange", source, target);
assert_eq!(
coverage(&p),
BTreeSet::from([
located("non_leaf_work_exchange", edge, "source"),
located("non_leaf_work_exchange", edge, "target"),
located("work_without_behavior_context", a, ""),
located("work_without_behavior_context", b, ""),
])
);
if let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = p.relations[&edge].body
{
assert_eq!(p.elements[&source_port].parent, Some(source));
assert_eq!(p.elements[&target_port].parent, Some(target));
assert!(
coverage(&p)
.iter()
.all(|(_, id, _)| *id != source_port && *id != target_port)
);
}
}
}
#[test]
fn grouping_and_scenario_only_capabilities_do_not_duplicate_support_warnings() {
let mut p = Project::empty();
let parent = add(&mut p, Kind::Capability, Layer::Oa, "Capability group");
let leaf = p
.add_element(Kind::Capability, Layer::Oa, "Leaf capability", Some(parent))
.unwrap();
let issues = validation::validate(&p);
assert!(
issues
.iter()
.any(|i| i.element == Some(leaf) && i.code == "unsupported_capability")
);
assert!(
!issues
.iter()
.any(|i| i.element == Some(parent) && i.code.contains("capability"))
);
assert!(coverage(&p).is_empty());
let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Capability scenario");
set_context(&mut p, scenario, leaf);
let issues = validation::validate(&p);
assert!(!issues.iter().any(|i| i.code == "unsupported_capability"));
assert!(
issues
.iter()
.any(|i| i.element == Some(scenario) && i.code == "empty_sequence")
);
assert!(coverage(&p).is_empty());
}
#[test]
fn structural_errors_suppress_completeness_cascades_and_rejection_keeps_the_model() {
let mut p = Project::empty();
let a = add(&mut p, Kind::Activity, Layer::Oa, "Request");
let b = add(&mut p, Kind::Activity, Layer::Oa, "Respond");
let e = p.relate(RelationKind::Interaction, "Request data", a, b);
let process = add(&mut p, Kind::Process, Layer::Oa, "Path");
let step = occurrence(&mut p, process, exchange(e));
assert!(coverage(&p).is_empty());
let mut bad = p.clone();
if let RelationBody::Interaction { source, .. } = &mut bad.relations.get_mut(&e).unwrap().body {
*source = Id::new_v4();
}
let mut wrong_owner = p.clone();
wrong_owner.elements.get_mut(&step).unwrap().parent = Some(a);
let mut recursive = p.clone();
let scenario = add(&mut recursive, Kind::Scenario, Layer::Oa, "Cycle");
occurrence(
&mut recursive,
scenario,
OccurrenceBody::ScenarioUse {
scenario: Some(scenario),
bindings: vec![],
},
);
for (candidate, expected_error) in [
(bad, "invalid_endpoints"),
(wrong_owner, "invalid_occurrence"),
(recursive, "recursive_scenario_use"),
] {
let issues = validation::validate(&candidate);
assert!(
issues
.iter()
.any(|i| i.severity == Severity::Error && i.code == expected_error),
"{issues:?}"
);
assert!(
!issues.iter().any(|i| CODES.contains(&i.code.as_str())),
"{issues:?}"
);
let mut session = Session::new(p.clone());
assert!(
session
.execute(Command::Batch(diff_commands(&p, &candidate)))
.is_err()
);
assert_eq!(session.project(), &p);
assert_eq!(session.revision(), 0);
}
}
#[test]
fn multifile_lint_preserves_core_codes_ids_and_actual_owning_source_paths() {
let source = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
("project.arcadia".into(), "arcadia 5; project p \"Coverage\" { include \"work.arcadia\"; include \"exchanges.arcadia\"; }".into()),
("work.arcadia".into(), "oa_activity a \"Request\" {}\noa_activity b \"Respond\" {}".into()),
("exchanges.arcadia".into(), "interaction e \"Data\" { source @a; target @b; }".into()),
]) };
let compiled = compile_bundle(&source, ProjectLimits::default()).unwrap();
let expected = coverage(&compiled.decoded.project);
assert_eq!(expected.len(), 3);
let diagnostics: Vec<_> = lint_bundle(&compiled.bundle, ProjectLimits::default())
.into_iter()
.filter(|i| CODES.contains(&i.diagnostic.code.as_str()))
.collect();
let actual: BTreeSet<_> = diagnostics
.iter()
.map(|i| {
assert_eq!(i.diagnostic.severity, Severity::Warning);
let id = i.diagnostic.object_id.unwrap();
assert_eq!(i.path, compiled.index.declarations[&id].path);
assert!(i.diagnostic.span.end > i.diagnostic.span.start);
(i.diagnostic.code.clone(), id, i.diagnostic.field.clone())
})
.collect();
assert_eq!(actual, expected);
assert_eq!(diagnostics.len(), expected.len());
}
// Authoring-equivalent complete record edits keep the test independent of a
// private completeness helper and exercise actual transaction/undo boundaries.
fn diff_commands(before: &Project, after: &Project) -> Vec<Command> {
let mut commands = Vec::new();
for (id, element) in &after.elements {
match before.elements.get(id) {
None => commands.push(Command::CreateElement(element.clone())),
Some(old) if old != element => {
commands.push(Command::EditElement(*id, element.clone()))
}
_ => {}
}
}
for (id, relation) in &after.relations {
match before.relations.get(id) {
None => commands.push(Command::CreateRelation(relation.clone())),
Some(old) if old != relation => {
commands.push(Command::EditRelation(*id, relation.clone()))
}
_ => {}
}
}
commands
}