use std::collections::BTreeMap;
use agm_core::loader::{self, LoadMode};
use agm_core::model::code::{CodeAction, CodeBlock};
use agm_core::model::execution::ExecutionStatus;
use agm_core::model::fields::{NodeType, Priority, Span};
use agm_core::model::file::{AgmFile, Header, LoadProfile};
use agm_core::model::node::Node;
use agm_core::parser;
fn fixture(relative: &str) -> String {
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest)
.join("tests/fixtures")
.join(relative);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read fixture {}: {e}", path.display()))
}
fn parse_fixture(relative: &str) -> AgmFile {
let src = fixture(relative);
parser::parse(&src).unwrap_or_else(|errs| {
panic!(
"parse failed for {relative}: {:?}",
errs.iter().map(|e| e.to_string()).collect::<Vec<_>>()
)
})
}
fn minimal_header() -> Header {
Header {
agm: "1.0".to_owned(),
package: "test.scale".to_owned(),
version: "0.1.0".to_owned(),
title: None,
owner: None,
imports: None,
default_load: None,
description: None,
tags: None,
status: None,
load_profiles: None,
target_runtime: None,
}
}
fn make_node(id: &str) -> Node {
Node {
id: id.to_owned(),
node_type: NodeType::Facts,
summary: format!("summary for {id}"),
priority: Some(Priority::High),
stability: None,
confidence: None,
status: None,
depends: None,
related_to: None,
replaces: None,
conflicts: None,
see_also: None,
items: Some(vec!["item1".to_owned()]),
steps: None,
fields: None,
input: None,
output: None,
detail: Some("full detail text".to_owned()),
rationale: None,
tradeoffs: None,
resolution: None,
examples: None,
notes: None,
code: Some(CodeBlock {
lang: Some("bash".to_owned()),
target: None,
action: CodeAction::Full,
body: "echo hi".to_owned(),
anchor: None,
old: None,
}),
code_blocks: None,
verify: None,
agent_context: None,
target: None,
execution_status: Some(ExecutionStatus::Pending),
executed_by: None,
executed_at: None,
execution_log: None,
retry_count: None,
parallel_groups: None,
memory: None,
scope: None,
applies_when: None,
valid_from: None,
valid_until: None,
tags: Some(vec!["tag1".to_owned()]),
aliases: None,
keywords: None,
extra_fields: BTreeMap::new(),
span: Span::new(1, 5),
}
}
#[test]
fn test_load_summary_mode_strips_operational_executable_full_fields() {
let file = parse_fixture("valid/fully_populated_node.agm");
let result = loader::load(&file, LoadMode::Summary);
let node = result.nodes.iter().find(|n| n.id == "auth.login").unwrap();
assert_eq!(node.id, "auth.login");
assert_eq!(node.node_type, NodeType::Workflow);
assert!(!node.summary.is_empty());
assert!(
node.priority.is_some(),
"priority should be present in Summary"
);
assert!(
node.stability.is_some(),
"stability should be present in Summary"
);
assert!(
node.depends.is_some(),
"depends should be present in Summary"
);
assert!(node.tags.is_some(), "tags should be present in Summary");
assert!(node.items.is_none(), "items must be absent in Summary");
assert!(node.steps.is_none(), "steps must be absent in Summary");
assert!(node.fields.is_none(), "fields must be absent in Summary");
assert!(node.input.is_none(), "input must be absent in Summary");
assert!(node.output.is_none(), "output must be absent in Summary");
assert!(node.code.is_none(), "code must be absent in Summary");
assert!(node.verify.is_none(), "verify must be absent in Summary");
assert!(
node.agent_context.is_none(),
"agent_context must be absent in Summary"
);
assert!(
node.execution_status.is_none(),
"execution_status must be absent in Summary"
);
assert!(node.memory.is_none(), "memory must be absent in Summary");
assert!(
node.confidence.is_none(),
"confidence must be absent in Summary"
);
assert!(node.detail.is_none(), "detail must be absent in Summary");
assert!(
node.rationale.is_none(),
"rationale must be absent in Summary"
);
assert!(
node.related_to.is_none(),
"related_to must be absent in Summary"
);
assert!(node.notes.is_none(), "notes must be absent in Summary");
}
#[test]
fn test_load_operational_mode_includes_operational_strips_executable_full() {
let file = parse_fixture("valid/fully_populated_node.agm");
let result = loader::load(&file, LoadMode::Operational);
let node = result.nodes.iter().find(|n| n.id == "auth.login").unwrap();
assert!(
node.items.is_some(),
"items should be present in Operational"
);
assert!(
node.steps.is_some(),
"steps should be present in Operational"
);
assert!(
node.fields.is_some(),
"fields should be present in Operational"
);
assert!(
node.input.is_some(),
"input should be present in Operational"
);
assert!(
node.output.is_some(),
"output should be present in Operational"
);
assert!(node.code.is_none(), "code must be absent in Operational");
assert!(
node.verify.is_none(),
"verify must be absent in Operational"
);
assert!(
node.execution_status.is_none(),
"execution_status must be absent in Operational"
);
assert!(
node.confidence.is_none(),
"confidence must be absent in Operational"
);
assert!(
node.detail.is_none(),
"detail must be absent in Operational"
);
assert!(
node.related_to.is_none(),
"related_to must be absent in Operational"
);
}
#[test]
fn test_load_executable_mode_includes_executable_strips_full() {
let file = parse_fixture("valid/fully_populated_node.agm");
let result = loader::load(&file, LoadMode::Executable);
let node = result.nodes.iter().find(|n| n.id == "auth.login").unwrap();
assert!(node.code.is_some(), "code should be present in Executable");
assert!(
node.verify.is_some(),
"verify should be present in Executable"
);
assert!(
node.agent_context.is_some(),
"agent_context should be present in Executable"
);
assert!(
node.execution_status.is_some(),
"execution_status should be present in Executable"
);
assert!(
node.memory.is_some(),
"memory should be present in Executable"
);
assert!(
node.confidence.is_none(),
"confidence must be absent in Executable"
);
assert!(node.detail.is_none(), "detail must be absent in Executable");
assert!(
node.related_to.is_none(),
"related_to must be absent in Executable"
);
assert!(node.notes.is_none(), "notes must be absent in Executable");
assert!(
node.parallel_groups.is_none(),
"parallel_groups must be absent in Executable"
);
}
#[test]
fn test_load_full_mode_includes_all_fields() {
let file = parse_fixture("valid/fully_populated_node.agm");
let result = loader::load(&file, LoadMode::Full);
let node = result.nodes.iter().find(|n| n.id == "auth.login").unwrap();
assert!(node.priority.is_some());
assert!(node.stability.is_some());
assert!(node.depends.is_some());
assert!(node.tags.is_some());
assert!(node.items.is_some());
assert!(node.steps.is_some());
assert!(node.fields.is_some());
assert!(node.input.is_some());
assert!(node.output.is_some());
assert!(node.code.is_some());
assert!(node.verify.is_some());
assert!(node.agent_context.is_some());
assert!(node.execution_status.is_some());
assert!(node.memory.is_some());
assert!(node.confidence.is_some());
assert!(node.detail.is_some());
assert!(node.rationale.is_some());
assert!(node.related_to.is_some());
assert!(node.notes.is_some());
assert!(node.parallel_groups.is_some());
assert!(node.aliases.is_some());
assert!(node.keywords.is_some());
assert!(node.scope.is_some());
assert!(node.applies_when.is_some());
assert!(node.valid_from.is_some());
assert!(node.valid_until.is_some());
}
fn make_file_with_n_nodes(n: usize) -> AgmFile {
let nodes = (0..n).map(|i| make_node(&format!("node.{i:04}"))).collect();
AgmFile {
header: minimal_header(),
nodes,
}
}
#[test]
fn test_load_200_nodes_summary_mode_all_nodes_present_fields_correct() {
let file = make_file_with_n_nodes(200);
let result = loader::load(&file, LoadMode::Summary);
assert_eq!(result.nodes.len(), 200, "all 200 nodes must be present");
for node in &result.nodes {
assert!(!node.id.is_empty(), "node id must be populated");
assert!(!node.summary.is_empty(), "summary must be populated");
assert!(node.items.is_none(), "items must be absent in Summary");
assert!(node.detail.is_none(), "detail must be absent in Summary");
assert!(node.code.is_none(), "code must be absent in Summary");
}
}
#[test]
fn test_load_200_nodes_full_mode_all_fields_preserved() {
let file = make_file_with_n_nodes(200);
let result = loader::load(&file, LoadMode::Full);
assert_eq!(result.nodes.len(), 200, "all 200 nodes must be present");
for node in &result.nodes {
assert!(node.items.is_some(), "items must be present in Full");
assert!(node.detail.is_some(), "detail must be present in Full");
assert!(node.code.is_some(), "code must be present in Full");
}
}
fn make_profiled_file(n: usize) -> AgmFile {
let types = [
NodeType::Workflow,
NodeType::Facts,
NodeType::Rules,
NodeType::Decision,
];
let priorities = [
Priority::Critical,
Priority::High,
Priority::Normal,
Priority::Low,
];
let nodes: Vec<Node> = (0..n)
.map(|i| {
let mut node = make_node(&format!("node.{i:04}"));
node.node_type = types[i % types.len()].clone();
node.priority = Some(priorities[i % priorities.len()].clone());
node.tags = Some(vec![format!("tag{}", i % 5)]);
node
})
.collect();
AgmFile {
header: minimal_header(),
nodes,
}
}
fn make_10_profiles() -> BTreeMap<String, LoadProfile> {
let filter_types = [
"workflow", "facts", "rules", "decision", "workflow", "facts", "rules", "decision",
"workflow", "facts",
];
(0..10)
.map(|i| {
let name = format!("profile_{i:02}");
let filter = format!("type in [{}]", filter_types[i]);
(
name,
LoadProfile {
filter,
estimated_tokens: None,
},
)
})
.collect()
}
#[test]
fn test_load_profile_10_named_profiles_each_filters_correctly() {
let mut file = make_profiled_file(20);
file.header.load_profiles = Some(make_10_profiles());
let expected_type_counts = [
("workflow", 5usize),
("facts", 5),
("rules", 5),
("decision", 5),
("workflow", 5),
("facts", 5),
("rules", 5),
("decision", 5),
("workflow", 5),
("facts", 5),
];
for (i, (expected_type_str, expected_count)) in expected_type_counts.iter().enumerate() {
let profile_name = format!("profile_{i:02}");
let result = loader::load_profile(&file, Some(&profile_name))
.unwrap_or_else(|e| panic!("profile {profile_name} failed: {e}"));
assert_eq!(
result.nodes.len(),
*expected_count,
"profile {profile_name} should return {expected_count} nodes of type {expected_type_str}"
);
let expected_node_type: NodeType = expected_type_str.parse().unwrap();
for node in &result.nodes {
assert_eq!(
node.node_type, expected_node_type,
"profile {profile_name}: unexpected node type {:?} for node {}",
node.node_type, node.id
);
}
}
}
#[test]
fn test_load_profile_default_load_with_10_profiles_resolves_correctly() {
let mut file = make_profiled_file(20);
let profiles = make_10_profiles();
file.header.default_load = Some("profile_02".to_owned());
file.header.load_profiles = Some(profiles);
let result = loader::load_profile(&file, None)
.expect("load_profile with None should resolve default_load");
assert_eq!(
result.nodes.len(),
5,
"default profile should return 5 rules nodes"
);
for node in &result.nodes {
assert_eq!(
node.node_type,
NodeType::Rules,
"all returned nodes should be type rules, got {:?} for {}",
node.node_type,
node.id
);
}
}