use std::collections::{HashMap, HashSet};
use serde::Serialize;
use crate::catalog::CatalogItem;
use crate::namespace;
fn make_by_name(items: &[CatalogItem]) -> HashMap<(&str, &str), Vec<usize>> {
let mut by_name: HashMap<(&str, &str), Vec<usize>> = HashMap::new();
for (i, item) in items.iter().enumerate() {
by_name
.entry((item.source.as_str(), item.name.as_str()))
.or_default()
.push(i);
}
by_name
}
fn item_edges(
node: usize,
items: &[CatalogItem],
by_name: &HashMap<(&str, &str), Vec<usize>>,
text: &str,
) -> Vec<usize> {
let item = &items[node];
let mut out: Vec<usize> = Vec::new();
for name in namespace::referenced_names(text) {
if let Some(matches) = by_name.get(&(item.source.as_str(), name.as_str())) {
for &m in matches {
if m != node && !out.contains(&m) {
out.push(m);
}
}
}
}
for r in namespace::sibling_reference_tokens(text) {
if r.token.starts_with("{{ns:") {
continue;
}
let Some(matches) = by_name.get(&(item.source.as_str(), r.name.as_str())) else {
continue;
};
let hits: Vec<usize> = matches
.iter()
.copied()
.filter(|&m| r.kind.is_none_or(|k| items[m].kind == k))
.collect();
if r.kind.is_none() && hits.len() > 1 {
continue;
}
for m in hits {
if m != node && !out.contains(&m) {
out.push(m);
}
}
}
for entry in &item.requires {
let Ok(r) = crate::resolve::parse_item_ref(entry) else {
continue;
};
if r.source.is_some() {
continue;
}
if let Some(matches) = by_name.get(&(item.source.as_str(), r.name.as_str())) {
if r.kind.is_none() && matches.len() > 1 {
continue;
}
for &m in matches {
let candidate = &items[m];
if r.kind.is_some_and(|k| candidate.kind != k) {
continue;
}
if m != node && !out.contains(&m) {
out.push(m);
}
}
}
}
out
}
pub(crate) fn reachable(
items: &[CatalogItem],
roots: &[usize],
read: impl Fn(&CatalogItem) -> String,
) -> HashSet<usize> {
let by_name = make_by_name(items);
let mut reached: HashSet<usize> = HashSet::new();
let mut expanded: HashSet<usize> = HashSet::new();
let mut work: Vec<usize> = roots.to_vec();
while let Some(node) = work.pop() {
if !expanded.insert(node) {
continue;
}
for dep in item_edges(node, items, &by_name, &read(&items[node])) {
if reached.insert(dep) {
work.push(dep);
}
}
}
reached
}
pub struct DepIndex<'a> {
items: &'a [CatalogItem],
by_name: HashMap<(&'a str, &'a str), Vec<usize>>,
}
impl<'a> DepIndex<'a> {
pub fn new(items: &'a [CatalogItem]) -> Self {
Self {
by_name: make_by_name(items),
items,
}
}
pub fn direct_keys(&self, node: usize, read: &impl Fn(&CatalogItem) -> String) -> Vec<String> {
let Some(item) = self.items.get(node) else {
return Vec::new();
};
let text = read(item);
item_edges(node, self.items, &self.by_name, &text)
.into_iter()
.map(|i| self.items[i].key().as_str().to_string())
.collect()
}
#[cfg(test)]
pub fn direct_keys_for(
&self,
item: &CatalogItem,
read: &impl Fn(&CatalogItem) -> String,
) -> Vec<String> {
match self.items.iter().position(|it| std::ptr::eq(it, item)) {
Some(node) => self.direct_keys(node, read),
None => Vec::new(),
}
}
}
pub struct Resolution {
roots: Vec<usize>,
deps: HashMap<usize, Vec<usize>>,
pulled: HashSet<usize>,
installed: HashSet<usize>,
order: Vec<usize>,
}
pub fn resolve(
items: &[CatalogItem],
selected: &[usize],
installed: &HashSet<String>,
read: impl Fn(&CatalogItem) -> String,
) -> Resolution {
let installed_idx: HashSet<usize> = (0..items.len())
.filter(|&i| installed.contains(items[i].key().as_str()))
.collect();
let mut source_items: HashMap<&str, Vec<usize>> = HashMap::new();
for (i, item) in items.iter().enumerate() {
source_items.entry(&item.source).or_default().push(i);
}
let selected_set: HashSet<usize> = selected.iter().copied().collect();
let mut expand_source: HashMap<&str, bool> = HashMap::new();
for (src, idxs) in &source_items {
let all_selected = idxs.iter().all(|i| selected_set.contains(i));
expand_source.insert(src, !all_selected);
}
let by_name = make_by_name(items);
let mut deps: HashMap<usize, Vec<usize>> = HashMap::new();
let mut edges_of = |node: usize| -> Vec<usize> {
if let Some(d) = deps.get(&node) {
return d.clone();
}
let item = &items[node];
let edges = if *expand_source.get(item.source.as_str()).unwrap_or(&true) {
let text = read(item);
item_edges(node, items, &by_name, &text)
} else {
Vec::new()
};
deps.insert(node, edges.clone());
edges
};
let mut visited: HashSet<usize> = HashSet::new();
let mut order: Vec<usize> = Vec::new();
let mut closure: HashSet<usize> = HashSet::new();
enum Frame {
Enter(usize),
Exit(usize),
}
for &root in selected {
if visited.contains(&root) {
continue;
}
let mut stack = vec![Frame::Enter(root)];
while let Some(frame) = stack.pop() {
match frame {
Frame::Enter(node) => {
if !visited.insert(node) {
continue;
}
closure.insert(node);
stack.push(Frame::Exit(node));
let children = edges_of(node);
for &child in children.iter().rev() {
if !visited.contains(&child) {
stack.push(Frame::Enter(child));
}
}
}
Frame::Exit(node) => {
if !installed_idx.contains(&node) {
order.push(node);
}
}
}
}
}
let pulled: HashSet<usize> = closure
.iter()
.copied()
.filter(|i| !selected_set.contains(i))
.collect();
Resolution {
roots: selected.to_vec(),
deps,
pulled,
installed: installed_idx,
order,
}
}
const MAX_RENDER_DEPTH: usize = 200;
const TRUNCATION_NOTICE: &str = "(truncated: max depth reached)";
fn push_truncation_notice(out: &mut String, depth: usize) {
for _ in 0..depth {
out.push_str(" ");
}
out.push_str("- ");
out.push_str(TRUNCATION_NOTICE);
out.push('\n');
}
impl Resolution {
pub fn install_order(&self) -> &[usize] {
&self.order
}
pub fn adds_dependencies(&self) -> bool {
!self.pulled.is_empty()
}
pub fn render_tree(&self, items: &[CatalogItem]) -> String {
let mut out = String::new();
let mut rendered: HashSet<usize> = HashSet::new();
for &root in &self.roots {
let mut path: Vec<usize> = Vec::new();
self.render_node(items, root, 0, &mut path, &mut out, &mut rendered);
}
out
}
#[allow(clippy::too_many_arguments)]
fn render_node(
&self,
items: &[CatalogItem],
node: usize,
depth: usize,
path: &mut Vec<usize>,
out: &mut String,
rendered: &mut HashSet<usize>,
) {
for _ in 0..depth {
out.push_str(" ");
}
out.push_str("- ");
out.push_str(&items[node].display_key());
if path.contains(&node) {
out.push_str(" (cycle)\n");
return;
}
if !rendered.insert(node) {
out.push_str(" (seen)\n");
return;
}
let role = if self.installed.contains(&node) {
"installed"
} else if self.pulled.contains(&node) {
"dep"
} else {
"selected"
};
out.push_str(" [");
out.push_str(role);
out.push_str("]\n");
if depth >= MAX_RENDER_DEPTH {
push_truncation_notice(out, depth + 1);
return;
}
path.push(node);
if let Some(children) = self.deps.get(&node) {
for &child in children {
self.render_node(items, child, depth + 1, path, out, rendered);
}
}
path.pop();
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DepNode {
pub key: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub cycle: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub seen: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Vec<DepNode>>,
}
impl DepNode {
pub fn normal(key: String, dependencies: Vec<DepNode>) -> Self {
Self {
key,
cycle: false,
seen: false,
dependencies: Some(dependencies),
}
}
fn cycle_leaf(key: String) -> Self {
Self {
key,
cycle: true,
seen: false,
dependencies: None,
}
}
fn seen_leaf(key: String) -> Self {
Self {
key,
cycle: false,
seen: true,
dependencies: None,
}
}
}
#[allow(dead_code)]
pub struct InstalledGraph {
nodes: Vec<CatalogItem>,
edges: Vec<Vec<usize>>,
key_to_idx: HashMap<String, usize>,
}
#[allow(dead_code)]
pub fn installed_graph(
items: &[CatalogItem],
installed: impl Fn(&CatalogItem) -> bool,
read: impl Fn(&CatalogItem) -> String,
) -> InstalledGraph {
let nodes: Vec<CatalogItem> = items.iter().filter(|it| installed(it)).cloned().collect();
let key_to_idx: HashMap<String, usize> = nodes
.iter()
.enumerate()
.map(|(i, it)| (it.key().as_str().to_string(), i))
.collect();
let by_name = make_by_name(items);
let mut edges: Vec<Vec<usize>> = vec![Vec::new(); nodes.len()];
let installed_to_full: HashMap<String, usize> = items
.iter()
.enumerate()
.filter(|(_, it)| installed(it))
.map(|(full_i, it)| (it.key().as_str().to_string(), full_i))
.collect();
for (installed_i, node_item) in nodes.iter().enumerate() {
let full_i = match installed_to_full.get(node_item.key().as_str()) {
Some(&idx) => idx,
None => continue,
};
let text = read(node_item);
let dep_full_indices = item_edges(full_i, items, &by_name, &text);
for dep_full_idx in dep_full_indices {
let dep_key = items[dep_full_idx].key();
if let Some(&dep_installed_i) = key_to_idx.get(dep_key.as_str())
&& dep_installed_i != installed_i
&& !edges[installed_i].contains(&dep_installed_i)
{
edges[installed_i].push(dep_installed_i);
}
}
}
InstalledGraph {
nodes,
edges,
key_to_idx,
}
}
#[allow(dead_code)]
impl InstalledGraph {
pub fn dependents(&self, target_key: &str) -> Vec<String> {
let Some(&target_idx) = self.key_to_idx.get(target_key) else {
return Vec::new();
};
let mut out = Vec::new();
for (i, dep_list) in self.edges.iter().enumerate() {
if dep_list.contains(&target_idx) {
let key = self.nodes[i].key().as_str().to_string();
if !out.contains(&key) {
out.push(key);
}
}
}
out
}
pub fn render_forest(&self) -> String {
let mut in_degree = vec![0usize; self.nodes.len()];
for dep_list in &self.edges {
for &dep_idx in dep_list {
in_degree[dep_idx] += 1;
}
}
let mut out = String::new();
let mut emitted: HashSet<usize> = HashSet::new();
let mut rendered: HashSet<usize> = HashSet::new();
for (i, °) in in_degree.iter().enumerate() {
if deg == 0 {
self.mark_reachable(i, &mut emitted);
let mut path = Vec::new();
self.render_installed_node(i, 0, &mut path, &mut out, &mut rendered);
}
}
for i in 0..self.nodes.len() {
if !emitted.contains(&i) {
self.mark_reachable(i, &mut emitted);
let mut path = Vec::new();
self.render_installed_node(i, 0, &mut path, &mut out, &mut rendered);
}
}
out
}
fn mark_reachable(&self, start: usize, emitted: &mut HashSet<usize>) {
let mut stack = vec![start];
while let Some(node) = stack.pop() {
if emitted.insert(node) {
for &child in &self.edges[node] {
if !emitted.contains(&child) {
stack.push(child);
}
}
}
}
}
pub fn render_subtree(&self, root_key: &str) -> Option<String> {
let &root_idx = self.key_to_idx.get(root_key)?;
let mut out = String::new();
let mut path = Vec::new();
let mut rendered: HashSet<usize> = HashSet::new();
self.render_installed_node(root_idx, 0, &mut path, &mut out, &mut rendered);
Some(out)
}
#[allow(clippy::too_many_arguments)]
fn render_installed_node(
&self,
node: usize,
depth: usize,
path: &mut Vec<usize>,
out: &mut String,
rendered: &mut HashSet<usize>,
) {
for _ in 0..depth {
out.push_str(" ");
}
out.push_str("- ");
out.push_str(&self.nodes[node].display_key());
if path.contains(&node) {
out.push_str(" (cycle)\n");
return;
}
if !rendered.insert(node) {
out.push_str(" (seen)\n");
return;
}
out.push('\n');
if depth >= MAX_RENDER_DEPTH {
push_truncation_notice(out, depth + 1);
return;
}
path.push(node);
for &child in &self.edges[node] {
self.render_installed_node(child, depth + 1, path, out, rendered);
}
path.pop();
}
pub fn forest_nodes(&self) -> Vec<DepNode> {
let mut in_degree = vec![0usize; self.nodes.len()];
for dep_list in &self.edges {
for &dep_idx in dep_list {
in_degree[dep_idx] += 1;
}
}
let mut emitted: HashSet<usize> = HashSet::new();
let mut rendered: HashSet<usize> = HashSet::new();
let mut roots: Vec<DepNode> = Vec::new();
for (i, °) in in_degree.iter().enumerate() {
if deg == 0 {
self.mark_reachable(i, &mut emitted);
let mut path = Vec::new();
roots.push(self.build_node(i, 0, &mut path, &mut rendered));
}
}
for i in 0..self.nodes.len() {
if !emitted.contains(&i) {
self.mark_reachable(i, &mut emitted);
let mut path = Vec::new();
roots.push(self.build_node(i, 0, &mut path, &mut rendered));
}
}
roots
}
pub fn subtree_node(&self, root_key: &str) -> Option<DepNode> {
let &root_idx = self.key_to_idx.get(root_key)?;
let mut path = Vec::new();
let mut rendered: HashSet<usize> = HashSet::new();
Some(self.build_node(root_idx, 0, &mut path, &mut rendered))
}
fn build_node(
&self,
node: usize,
depth: usize,
path: &mut Vec<usize>,
rendered: &mut HashSet<usize>,
) -> DepNode {
let key = self.nodes[node].display_key();
if path.contains(&node) {
return DepNode::cycle_leaf(key);
}
if !rendered.insert(node) {
return DepNode::seen_leaf(key);
}
if depth >= MAX_RENDER_DEPTH {
return DepNode::normal(
key,
vec![DepNode::normal(TRUNCATION_NOTICE.to_string(), Vec::new())],
);
}
path.push(node);
let children: Vec<DepNode> = self.edges[node]
.iter()
.map(|&child| self.build_node(child, depth + 1, path, rendered))
.collect();
path.pop();
DepNode::normal(key, children)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ItemKind;
use std::collections::HashMap;
use std::path::PathBuf;
fn direct_dependency_keys(
item: &CatalogItem,
items: &[CatalogItem],
read: &impl Fn(&CatalogItem) -> String,
) -> Vec<String> {
DepIndex::new(items).direct_keys_for(item, read)
}
fn item(kind: ItemKind, name: &str, source: &str) -> CatalogItem {
CatalogItem {
kind,
name: name.to_string(),
source: source.to_string(),
prefix: None,
path: PathBuf::from("/tmp/fake"),
description: None,
when_to_use: None,
link_rel: None,
bin: None,
build: None,
requires: Vec::new(),
expand: Vec::new(),
hooks: Vec::new(),
ignore: None,
}
}
fn reader(map: HashMap<String, String>) -> impl Fn(&CatalogItem) -> String {
move |it: &CatalogItem| -> String {
map.get(&format!("{}:{}", it.kind.as_str(), it.name))
.cloned()
.unwrap_or_default()
}
}
fn no_installed() -> HashSet<String> {
HashSet::new()
}
fn ns_token(name: &str) -> String {
let mut s = String::new();
s.push_str("{{ns:");
s.push_str(name);
s.push_str("}}");
s
}
#[test]
fn skill_pulls_referenced_agent_before_itself() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "hand off to {{ns:test}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
assert_eq!(r.roots, vec![0]);
assert_eq!(r.install_order(), &[1, 0]);
assert!(r.adds_dependencies());
}
#[test]
fn path_tokens_are_dependency_edges() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Tool, "detect", "s"),
item(ItemKind::Resource, "allowlist.json", "s"),
item(ItemKind::Agent, "planner", "s"),
item(ItemKind::Rule, "unused", "s"),
];
let mut content = HashMap::new();
content.insert(
"skill:review".into(),
"{{self}}/x {{tools:detect}} {{ resources:allowlist.json }} {{path:agent:planner}}"
.into(),
);
let r = resolve(&items, &[0], &no_installed(), reader(content));
let mut order = r.install_order().to_vec();
order.sort();
assert_eq!(order, vec![0, 1, 2, 3], "rule:unused is not referenced");
}
#[test]
fn ambiguous_bare_path_token_forms_no_edge() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Tool, "x", "s"),
item(ItemKind::Resource, "x", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Tool, "only", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{path:x}}".into());
content.insert("skill:b".into(), "{{path:only}} {{resources:x}}".into());
content.insert("skill:c".into(), "{{resources:only}}".into());
let read = reader(content);
assert_eq!(
resolve(&items, &[0], &no_installed(), &read).install_order(),
&[0]
);
let mut order = resolve(&items, &[3], &no_installed(), &read)
.install_order()
.to_vec();
order.sort();
assert_eq!(order, vec![2, 3, 4]);
assert_eq!(
resolve(&items, &[5], &no_installed(), &read).install_order(),
&[5]
);
}
#[test]
fn reachable_follows_edges_from_roots_only() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Resource, "templates", "s"),
item(ItemKind::Resource, "footer.md", "s"),
item(ItemKind::Resource, "orphan", "s"),
item(ItemKind::Resource, "orphans-ref", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{resources:templates}}".into());
content.insert(
"resource:templates".into(),
"{{resources:footer.md}}".into(),
);
content.insert("resource:orphan".into(), "{{resources:orphans-ref}}".into());
let reached = reachable(&items, &[0], reader(content));
let mut got: Vec<usize> = reached.into_iter().collect();
got.sort();
assert_eq!(got, vec![1, 2]);
}
#[test]
fn references_never_cross_sources() {
let items = vec![
item(ItemKind::Skill, "review", "a"), item(ItemKind::Agent, "test", "b"), ];
let mut content = HashMap::new();
content.insert("skill:review".into(), "see {{ns:test}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
assert_eq!(r.install_order(), &[0]);
assert!(!r.adds_dependencies());
}
#[test]
fn transitive_chain_resolves_dependency_first() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:c}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
assert_eq!(r.install_order(), &[2, 1, 0]);
assert!(r.adds_dependencies());
}
#[test]
fn selecting_whole_source_adds_nothing() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Agent, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let r = resolve(&items, &[0, 1], &no_installed(), reader(content));
assert!(!r.adds_dependencies());
let mut order = r.install_order().to_vec();
order.sort_unstable();
assert_eq!(order, vec![0, 1]);
}
#[test]
fn no_token_selection_is_unchanged() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let content = HashMap::new(); let r = resolve(&items, &[0], &no_installed(), reader(content));
assert_eq!(r.install_order(), &[0]);
assert!(!r.adds_dependencies());
}
#[test]
fn cycle_terminates_with_back_edge_and_stable_order() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert_eq!(order.len(), 2);
assert!(order.contains(&0) && order.contains(&1));
assert_eq!(order, vec![1, 0]);
let tree = r.render_tree(&items);
assert!(
tree.contains("(cycle)"),
"tree should mark a back-edge: {tree}"
);
}
#[test]
fn already_installed_dep_excluded_from_order_but_shown_in_tree() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let mut installed = HashSet::new();
installed.insert("agent:test".to_string());
let r = resolve(&items, &[0], &installed, reader(content));
assert_eq!(r.install_order(), &[0]);
let tree = r.render_tree(&items);
assert!(
tree.contains("agent:test [installed]"),
"installed dep must be shown marked: {tree}"
);
}
#[test]
fn tree_roots_are_selected_items() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let tree = r.render_tree(&items);
assert!(
tree.starts_with("- skill:review [selected]\n"),
"root must be the selected item: {tree}"
);
assert!(
tree.contains(" - agent:test [dep]\n"),
"dependency must be an indented [dep] descendant: {tree}"
);
}
#[test]
fn one_token_can_match_several_siblings_across_kinds() {
let items = vec![
item(ItemKind::Skill, "root", "s"),
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let mut content = HashMap::new();
content.insert("skill:root".into(), "{{ns:shared}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order();
assert!(order.contains(&1) && order.contains(&2));
assert_eq!(*order.last().unwrap(), 0);
}
#[test]
fn diamond_shares_one_install_of_the_common_dependency() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
item(ItemKind::Skill, "d", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}} {{ns:c}}".into());
content.insert("skill:b".into(), "{{ns:d}}".into());
content.insert("skill:c".into(), "{{ns:d}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert_eq!(order.iter().filter(|&&n| n == 3).count(), 1);
let pos = |n: usize| order.iter().position(|&x| x == n).unwrap();
assert!(pos(3) < pos(1), "d before b: {order:?}");
assert!(pos(3) < pos(2), "d before c: {order:?}");
assert!(pos(1) < pos(0), "b before a: {order:?}");
assert!(pos(2) < pos(0), "c before a: {order:?}");
assert_eq!(order, vec![3, 1, 2, 0]);
assert_eq!(order.len(), 4);
assert!(r.adds_dependencies());
}
#[test]
fn multiple_roots_across_sources_resolve_per_source_independently() {
let items = vec![
item(ItemKind::Skill, "root", "x"), item(ItemKind::Agent, "helper", "x"), item(ItemKind::Rule, "spare", "x"), item(ItemKind::Skill, "root", "y"), item(ItemKind::Agent, "helper", "y"), item(ItemKind::Rule, "spare", "y"), ];
let mut content = HashMap::new();
content.insert("skill:root".into(), "{{ns:helper}}".into());
let r = resolve(&items, &[0, 3], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert!(!order.contains(&2) && !order.contains(&5));
let pos = |n: usize| order.iter().position(|&x| x == n);
assert!(
pos(1).unwrap() < pos(0).unwrap(),
"x-helper before x-root: {order:?}"
);
assert!(
pos(4).unwrap() < pos(3).unwrap(),
"y-helper before y-root: {order:?}"
);
assert_eq!(order, vec![1, 0, 4, 3]);
assert!(r.adds_dependencies());
}
#[test]
fn selected_root_that_is_already_installed_is_a_root_but_not_reinstalled() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let mut installed = HashSet::new();
installed.insert("skill:review".to_string()); let r = resolve(&items, &[0], &installed, reader(content));
assert_eq!(r.install_order(), &[1]);
let tree = r.render_tree(&items);
assert!(
tree.starts_with("- skill:review [installed]\n"),
"installed selected root must still head the tree: {tree}"
);
assert!(
tree.contains(" - agent:test [dep]\n"),
"dep of an installed root is still shown: {tree}"
);
}
#[test]
fn adds_dependencies_true_even_when_pulled_dep_is_already_installed() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let mut installed = HashSet::new();
installed.insert("agent:test".to_string());
let r = resolve(&items, &[0], &installed, reader(content));
assert_eq!(r.install_order(), &[0]);
assert!(
r.adds_dependencies(),
"a pulled-in (already-installed) dep still counts as adding deps"
);
}
#[test]
fn render_tree_exact_nested_format_is_locked() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
item(ItemKind::Skill, "d", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}} {{ns:c}}".into());
content.insert("skill:b".into(), "{{ns:d}}".into());
content.insert("skill:c".into(), "{{ns:d}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let expected = "\
- skill:a [selected]
- skill:b [dep]
- skill:d [dep]
- skill:c [dep]
- skill:d (seen)
";
assert_eq!(r.render_tree(&items), expected);
}
#[test]
fn render_tree_exact_cycle_back_edge_format_is_locked() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let expected = "\
- skill:a [selected]
- skill:b [dep]
- skill:a (cycle)
";
assert_eq!(r.render_tree(&items), expected);
}
#[test]
fn token_with_no_matching_sibling_is_a_non_edge() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "see {{ns:nonexistent}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
assert_eq!(r.install_order(), &[0]);
assert!(!r.adds_dependencies());
}
#[test]
fn self_reference_token_forms_no_edge() {
let items = vec![item(ItemKind::Skill, "solo", "s")];
let mut content = HashMap::new();
content.insert("skill:solo".into(), "I call {{ns:solo}} recursively".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
assert_eq!(r.install_order(), &[0]);
assert!(!r.adds_dependencies());
assert_eq!(r.render_tree(&items), "- skill:solo [selected]\n");
}
#[test]
fn requires_entry_adds_a_dependency_edge() {
let mut skill = item(ItemKind::Skill, "review", "s");
skill.requires = vec!["agent:test".to_string()];
let items = vec![skill, item(ItemKind::Agent, "test", "s")];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
assert_eq!(
r.install_order(),
&[1, 0],
"dep (1) must precede the skill (0)"
);
assert!(r.adds_dependencies());
}
#[test]
fn requires_and_token_same_dep_deduped_to_one_edge() {
let mut skill = item(ItemKind::Skill, "review", "s");
skill.requires = vec!["agent:test".to_string()];
let items = vec![skill, item(ItemKind::Agent, "test", "s")];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert_eq!(
order.iter().filter(|&&n| n == 1).count(),
1,
"dep must appear once"
);
assert_eq!(order, vec![1, 0]);
}
#[test]
fn requires_ambiguous_bare_name_emits_no_edge() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["shared".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
let order = r.install_order().to_vec();
assert!(
!order.contains(&1) && !order.contains(&2),
"ambiguous bare requires must not fan out to all kinds: {order:?}"
);
assert_eq!(
order,
vec![0],
"only root installs when requires is ambiguous: {order:?}"
);
assert!(!r.adds_dependencies());
}
#[test]
fn requires_bare_name_unique_match_resolves_to_single_edge() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["only".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "only", "s"), item(ItemKind::Agent, "other", "s"),
];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
let order = r.install_order().to_vec();
assert!(
order.contains(&1),
"unique bare requires must pull the match: {order:?}"
);
assert!(
!order.contains(&2),
"unrelated sibling must not be pulled: {order:?}"
);
assert_eq!(order, vec![1, 0], "dep (1) before root (0): {order:?}");
assert!(r.adds_dependencies());
}
#[test]
fn token_ambiguous_bare_name_still_matches_all_kinds() {
let items = vec![
item(ItemKind::Skill, "root", "s"),
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let mut content = HashMap::new();
content.insert("skill:root".into(), "{{ns:shared}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert!(
order.contains(&1) && order.contains(&2),
"{{{{ns:shared}}}} token must match BOTH kinds (DEP-3): {order:?}"
);
assert_eq!(*order.last().unwrap(), 0, "root is last: {order:?}");
}
#[test]
fn requires_kind_prefix_narrows_to_that_kind() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["agent:shared".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "shared", "s"), item(ItemKind::Rule, "shared", "s"), ];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
let order = r.install_order().to_vec();
assert!(order.contains(&1), "agent:shared must be pulled: {order:?}");
assert!(
!order.contains(&2),
"rule:shared must NOT be pulled: {order:?}"
);
}
#[test]
fn requires_source_qualified_entry_contributes_no_edge() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["owner/repo#agent:test".to_string()];
let items = vec![skill, item(ItemKind::Agent, "test", "s")];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
assert_eq!(
r.install_order(),
&[0],
"source-qualified entry must not pull the dep"
);
assert!(!r.adds_dependencies());
}
#[test]
fn same_bare_name_different_kind_is_a_distinct_dependency() {
let items = vec![
item(ItemKind::Skill, "root", "s"),
item(ItemKind::Agent, "test", "s"),
item(ItemKind::Rule, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:root".into(), "{{ns:test}}".into());
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert_eq!(order.len(), 3);
let pos = |n: usize| order.iter().position(|&x| x == n).unwrap();
assert!(
pos(1) < pos(0) && pos(2) < pos(0),
"both kinds before root: {order:?}"
);
let tree = r.render_tree(&items);
assert!(tree.contains(" - agent:test [dep]\n"), "{tree}");
assert!(tree.contains(" - rule:test [dep]\n"), "{tree}");
}
#[test]
fn requires_chain_resolves_transitively_dependency_first() {
let mut a = item(ItemKind::Skill, "a", "s");
a.requires = vec!["skill:b".to_string()];
let mut b = item(ItemKind::Skill, "b", "s");
b.requires = vec!["skill:c".to_string()];
let c = item(ItemKind::Skill, "c", "s");
let items = vec![a, b, c];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
assert_eq!(
r.install_order(),
&[2, 1, 0],
"transitive requires chain must order c before b before a"
);
assert!(r.adds_dependencies());
}
#[test]
fn empty_requires_yields_no_edge() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
assert_eq!(r.install_order(), &[0]);
assert!(!r.adds_dependencies());
}
#[test]
fn requires_edge_is_a_noop_when_whole_source_selected() {
let mut a = item(ItemKind::Skill, "a", "s");
a.requires = vec!["agent:b".to_string()];
let items = vec![a, item(ItemKind::Agent, "b", "s")];
let r = resolve(&items, &[0, 1], &no_installed(), reader(HashMap::new()));
assert!(
!r.adds_dependencies(),
"a requires edge must be a no-op under full-source selection"
);
let mut order = r.install_order().to_vec();
order.sort_unstable();
assert_eq!(order, vec![0, 1]);
}
#[test]
fn requires_self_reference_forms_no_edge() {
let mut solo = item(ItemKind::Skill, "solo", "s");
solo.requires = vec!["skill:solo".to_string()];
let items = vec![solo];
let r = resolve(&items, &[0], &no_installed(), reader(HashMap::new()));
assert_eq!(r.install_order(), &[0]);
assert!(!r.adds_dependencies());
assert_eq!(r.render_tree(&items), "- skill:solo [selected]\n");
}
#[test]
fn requires_already_installed_dep_excluded_but_still_pulled() {
let mut review = item(ItemKind::Skill, "review", "s");
review.requires = vec!["agent:test".to_string()];
let items = vec![review, item(ItemKind::Agent, "test", "s")];
let mut installed = HashSet::new();
installed.insert("agent:test".to_string());
let r = resolve(&items, &[0], &installed, reader(HashMap::new()));
assert_eq!(
r.install_order(),
&[0],
"an already-installed requires dep must not be reinstalled"
);
assert!(
r.adds_dependencies(),
"the pulled-in (installed) requires dep still counts as adding deps"
);
let tree = r.render_tree(&items);
assert!(
tree.contains("agent:test [installed]"),
"installed requires dep must be shown marked: {tree}"
);
}
#[test]
fn dep_index_direct_keys_match_the_per_call_wrapper_for_every_item() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
item(ItemKind::Rule, "style", "s"),
item(ItemKind::Skill, "review", "other"),
];
let mut content = HashMap::new();
content.insert(
"skill:review".into(),
"see {{ns:test}} and {{ns:style}}".into(),
);
content.insert("agent:test".into(), "requires: style\n".into());
let read = reader(content);
let index = DepIndex::new(&items);
for (node, it) in items.iter().enumerate() {
assert_eq!(
index.direct_keys(node, &read),
direct_dependency_keys(it, &items, &read),
"indexed and per-call keys must agree for {}",
it.key().as_str()
);
}
assert_eq!(
index.direct_keys(0, &read),
vec!["agent:test".to_string(), "rule:style".to_string()]
);
assert!(index.direct_keys(items.len(), &read).is_empty());
}
#[test]
fn dep_index_direct_keys_for_a_foreign_item_is_empty() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let outsider = item(ItemKind::Skill, "review", "s");
let mut content = HashMap::new();
content.insert("skill:review".into(), "hand off to {{ns:test}}".into());
let read = reader(content);
let index = DepIndex::new(&items);
assert_eq!(
index.direct_keys_for(&items[0], &read),
vec!["agent:test".to_string()]
);
assert!(index.direct_keys_for(&outsider, &read).is_empty());
}
#[test]
fn direct_dependency_keys_ns_token_edge() {
let items = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let mut content = HashMap::new();
content.insert("skill:review".into(), "hand off to {{ns:test}}".into());
let read = reader(content);
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(keys, vec!["agent:test".to_string()]);
}
#[test]
fn direct_dependency_keys_requires_edge() {
let mut skill = item(ItemKind::Skill, "review", "s");
skill.requires = vec!["agent:test".to_string()];
let items = vec![skill, item(ItemKind::Agent, "test", "s")];
let read = reader(HashMap::new());
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(keys, vec!["agent:test".to_string()]);
}
#[test]
fn direct_dependency_keys_union_deduped() {
let mut skill = item(ItemKind::Skill, "review", "s");
skill.requires = vec!["agent:test".to_string()];
let items = vec![skill, item(ItemKind::Agent, "test", "s")];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let read = reader(content);
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(keys, vec!["agent:test".to_string()]);
assert_eq!(keys.len(), 1, "must be deduped to one key");
}
#[test]
fn direct_dependency_keys_kind_narrowing() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["agent:shared".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let read = reader(HashMap::new());
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(keys, vec!["agent:shared".to_string()]);
assert!(
!keys.contains(&"rule:shared".to_string()),
"rule:shared must not be included with kind-narrowed requires"
);
}
#[test]
fn direct_dependency_keys_within_source_only() {
let items = vec![
item(ItemKind::Skill, "review", "a"),
item(ItemKind::Agent, "test", "b"), ];
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let read = reader(content);
let keys = direct_dependency_keys(&items[0], &items, &read);
assert!(
keys.is_empty(),
"cross-source token must yield no dependency key"
);
}
#[test]
fn installed_dependents_direct_dep_reported() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let deps = g.dependents("skill:b");
assert_eq!(deps, vec!["skill:a".to_string()]);
}
#[test]
fn installed_graph_source_scoped_membership_skips_same_key_twin() {
let items = vec![
item(ItemKind::Skill, "review", "a"),
item(ItemKind::Skill, "review", "b"),
];
let g = installed_graph(
&items,
|it| it.key().as_str() == "skill:review" && it.source == "a",
reader(HashMap::new()),
);
let roots = g.forest_nodes();
assert_eq!(
roots.len(),
1,
"one installed copy must yield one node, got {roots:?}"
);
}
#[test]
fn installed_dependents_non_depended_item_returns_empty() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let deps = g.dependents("skill:a");
assert!(
deps.is_empty(),
"nothing depends on a, so dependents must be empty: {deps:?}"
);
}
#[test]
fn installed_dependents_non_installed_edge_not_shown() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let deps = g.dependents("skill:b");
assert!(
deps.is_empty(),
"non-installed item is not a graph node: {deps:?}"
);
}
#[test]
fn installed_dependents_reverse_direction_only() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
assert!(
!g.dependents("skill:a").contains(&"skill:b".to_string()),
"b must not appear as a dependent of a"
);
}
#[test]
fn installed_dependents_requires_edge() {
let mut a = item(ItemKind::Skill, "a", "s");
a.requires = vec!["skill:b".to_string()];
let items = vec![a, item(ItemKind::Skill, "b", "s")];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
let deps = g.dependents("skill:b");
assert_eq!(deps, vec!["skill:a".to_string()]);
}
#[test]
fn forest_roots_are_items_with_no_incoming_edge() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let forest = g.render_forest();
assert!(
forest.starts_with("- skill:a\n"),
"a must be the root line: {forest:?}"
);
assert!(
forest.contains(" - skill:b\n"),
"b must be nested under a: {forest:?}"
);
let root_b = forest.lines().any(|l| l == "- skill:b");
assert!(!root_b, "b must not be its own root line: {forest:?}");
}
#[test]
fn forest_transitive_nesting() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:c}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let expected = "- skill:a\n - skill:b\n - skill:c\n";
assert_eq!(g.render_forest(), expected);
}
#[test]
fn forest_cycle_marks_back_edge() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let forest = g.render_forest();
assert!(
forest.contains("(cycle)"),
"render_forest must mark a back-edge as (cycle): {forest:?}"
);
assert!(!forest.is_empty());
let sub = g.render_subtree("skill:a").expect("skill:a must be a node");
assert!(
sub.contains("(cycle)"),
"render_subtree must mark a back-edge as (cycle): {sub:?}"
);
}
#[test]
fn forest_render_subtree_scopes_to_one_root() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:c}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let sub = g.render_subtree("skill:a").expect("skill:a must be a node");
let expected = "- skill:a\n - skill:b\n - skill:c\n";
assert_eq!(sub, expected);
}
#[test]
fn forest_render_subtree_none_for_non_node() {
let items = vec![item(ItemKind::Skill, "a", "s")];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
assert!(
g.render_subtree("skill:nonexistent").is_none(),
"non-installed key must return None"
);
}
#[test]
fn forest_exact_format_locked() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:c}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let expected = "\
- skill:a
- skill:b
- skill:c
";
assert_eq!(g.render_forest(), expected);
}
#[test]
fn forest_cycle_exact_format_locked() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let expected = "\
- skill:a
- skill:b
- skill:a (cycle)
";
assert_eq!(
g.render_forest(),
expected,
"pure 2-cycle must render with the lowest-index node promoted as root"
);
let sub = g.render_subtree("skill:a").expect("skill:a must be a node");
assert_eq!(sub, expected, "subtree of a must match the forest root");
}
#[test]
fn direct_dependency_keys_item_not_in_catalog_is_empty() {
let in_catalog = vec![
item(ItemKind::Skill, "review", "s"),
item(ItemKind::Agent, "test", "s"),
];
let outsider = item(ItemKind::Skill, "review", "s");
let mut content = HashMap::new();
content.insert("skill:review".into(), "{{ns:test}}".into());
let read = reader(content);
let keys = direct_dependency_keys(&outsider, &in_catalog, &read);
assert!(
keys.is_empty(),
"an item not present in `items` must yield no keys, not panic: {keys:?}"
);
}
#[test]
fn direct_dependency_keys_prefixed_item_keys_use_effective_name() {
let mut review = item(ItemKind::Skill, "review", "s");
review.prefix = Some("jk".to_string());
let mut test = item(ItemKind::Agent, "test", "s");
test.prefix = Some("jk".to_string());
let items = vec![review, test];
let mut content = HashMap::new();
content.insert("skill:review".into(), "hand off to {{ns:test}}".into());
let read = reader(content);
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(
keys,
vec!["agent:jk:test".to_string()],
"dep key must use the effective (prefixed) name while the token resolved by bare name"
);
}
#[test]
fn direct_dependency_keys_multiple_tokens_in_stable_discovery_order() {
let items = vec![
item(ItemKind::Skill, "root", "s"),
item(ItemKind::Agent, "alpha", "s"),
item(ItemKind::Agent, "beta", "s"),
item(ItemKind::Agent, "gamma", "s"),
];
let mut content = HashMap::new();
content.insert(
"skill:root".into(),
"first {{ns:gamma}} then {{ns:alpha}} then {{ns:beta}} again {{ns:alpha}}".into(),
);
let read = reader(content);
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(
keys,
vec![
"agent:gamma".to_string(),
"agent:alpha".to_string(),
"agent:beta".to_string(),
],
"keys must follow token discovery order and be deduped: {keys:?}"
);
}
#[test]
fn installed_graph_diamond_forest_nests_shared_dep_under_both_dependents() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
item(ItemKind::Skill, "d", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}} {{ns:c}}".into());
content.insert("skill:b".into(), "{{ns:d}}".into());
content.insert("skill:c".into(), "{{ns:d}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
installed_keys.insert("skill:d".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let expected = "\
- skill:a
- skill:b
- skill:d
- skill:c
- skill:d (seen)
";
assert_eq!(
g.render_forest(),
expected,
"diamond forest must nest the shared dep under each dependent with a as sole root"
);
assert!(
!g.render_forest().lines().any(|l| l == "- skill:d"),
"shared dependency d must not appear as its own root"
);
}
#[test]
fn installed_graph_excludes_catalog_items_with_no_installed_key() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "ghost", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}} {{ns:ghost}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let forest = g.render_forest();
assert!(
!forest.contains("ghost"),
"uninstalled catalog item must not appear in the forest: {forest:?}"
);
assert_eq!(forest, "- skill:a\n - skill:b\n");
assert!(
g.render_subtree("skill:ghost").is_none(),
"uninstalled item is not a node: render_subtree must be None"
);
assert!(g.dependents("skill:ghost").is_empty());
}
#[test]
fn installed_graph_prefixed_items_resolve_edges_by_effective_keys() {
let mut a = item(ItemKind::Skill, "a", "s");
a.prefix = Some("jk".to_string());
let mut b = item(ItemKind::Skill, "b", "s");
b.prefix = Some("jk".to_string());
let items = vec![a, b];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:jk:a".to_string());
installed_keys.insert("skill:jk:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
assert_eq!(
g.dependents("skill:jk:b"),
vec!["skill:jk:a".to_string()],
"edge must resolve between prefixed nodes by effective key"
);
assert_eq!(
g.render_forest(),
"- skill:jk:a\n - skill:jk:b\n",
"forest must render the prefixed effective keys"
);
}
#[test]
fn installed_dependents_multiple_dependents_all_returned() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "shared", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:shared}}".into());
content.insert("skill:b".into(), "{{ns:shared}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:shared".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let deps = g.dependents("skill:shared");
assert_eq!(
deps,
vec!["skill:a".to_string(), "skill:b".to_string()],
"all dependents of the shared target must be returned in index order"
);
}
#[test]
fn installed_dependents_target_with_no_dependents_in_larger_graph() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
item(ItemKind::Skill, "d", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}} {{ns:c}}".into());
content.insert("skill:b".into(), "{{ns:d}}".into());
content.insert("skill:c".into(), "{{ns:d}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
installed_keys.insert("skill:d".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
assert!(
g.dependents("skill:a").is_empty(),
"the top root must have no dependents in a diamond"
);
assert_eq!(
g.dependents("skill:d"),
vec!["skill:b".to_string(), "skill:c".to_string()]
);
}
#[test]
fn forest_dependency_only_item_never_appears_as_root_two_dependents() {
let items = vec![
item(ItemKind::Skill, "r1", "s"),
item(ItemKind::Skill, "r2", "s"),
item(ItemKind::Skill, "lib", "s"),
];
let mut content = HashMap::new();
content.insert("skill:r1".into(), "{{ns:lib}}".into());
content.insert("skill:r2".into(), "{{ns:lib}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:r1".to_string());
installed_keys.insert("skill:r2".to_string());
installed_keys.insert("skill:lib".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let expected = "\
- skill:r1
- skill:lib
- skill:r2
- skill:lib (seen)
";
assert_eq!(
g.render_forest(),
expected,
"lib must nest under both roots and never appear as its own top-level root"
);
assert!(
!g.render_forest().lines().any(|l| l == "- skill:lib"),
"dependency-only item must not be a root line"
);
}
#[test]
fn render_subtree_from_dependency_only_node_renders_its_own_subtree() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:c}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let sub = g.render_subtree("skill:b").expect("skill:b must be a node");
assert_eq!(
sub, "- skill:b\n - skill:c\n",
"subtree of a dependency-only node is rooted at that node, ignoring in-degree"
);
}
#[test]
fn forest_all_cycle_component_promoted_alongside_independent_root() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let forest = g.render_forest();
let expected = "\
- skill:c
- skill:a
- skill:b
- skill:a (cycle)
";
assert_eq!(
forest, expected,
"independent root then promoted cycle root: all items must appear"
);
assert!(
forest.contains("skill:a") && forest.contains("skill:b") && forest.contains("skill:c"),
"no installed item may be hidden from the forest"
);
}
#[test]
fn forest_independent_items_both_roots() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
let forest = g.render_forest();
assert!(
forest.contains("- skill:a\n"),
"a must be a root: {forest:?}"
);
assert!(
forest.contains("- skill:b\n"),
"b must be a root: {forest:?}"
);
}
#[test]
fn forest_nodes_simple_chain_structured() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
assert_eq!(nodes.len(), 1, "expected 1 root: {nodes:?}");
let root = &nodes[0];
assert_eq!(root.key, "skill:a");
assert!(!root.cycle, "root must not be a cycle leaf");
let children = root
.dependencies
.as_ref()
.expect("root must have dependencies");
assert_eq!(children.len(), 1);
assert_eq!(children[0].key, "skill:b");
assert!(!children[0].cycle);
let leaf_deps = children[0]
.dependencies
.as_ref()
.expect("leaf must have dependencies");
assert!(
leaf_deps.is_empty(),
"leaf must have empty dependencies: {leaf_deps:?}"
);
}
#[test]
fn forest_nodes_cycle_yields_cycle_leaf() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
assert_eq!(nodes.len(), 1);
let root = &nodes[0];
assert_eq!(root.key, "skill:a");
assert!(!root.cycle);
let children = root.dependencies.as_ref().unwrap();
assert_eq!(children.len(), 1);
let b_node = &children[0];
assert_eq!(b_node.key, "skill:b");
assert!(!b_node.cycle);
let b_children = b_node.dependencies.as_ref().unwrap();
assert_eq!(b_children.len(), 1);
let cycle_leaf = &b_children[0];
assert_eq!(cycle_leaf.key, "skill:a");
assert!(cycle_leaf.cycle, "back-edge to a must be a cycle leaf");
assert!(
cycle_leaf.dependencies.is_none(),
"cycle leaf must have no dependencies field"
);
}
#[test]
fn subtree_node_returns_scoped_node() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let node = g.subtree_node("skill:a").expect("skill:a must be a node");
assert_eq!(node.key, "skill:a");
let children = node.dependencies.as_ref().unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].key, "skill:b");
assert!(g.subtree_node("skill:ghost").is_none());
}
#[test]
fn subtree_node_leaf_has_empty_dependencies() {
let items = vec![item(ItemKind::Skill, "leaf", "s")];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:leaf".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
let node = g.subtree_node("skill:leaf").expect("leaf must be a node");
assert_eq!(node.key, "skill:leaf");
assert!(!node.cycle);
let deps = node
.dependencies
.as_ref()
.expect("leaf must have dependencies field");
assert!(deps.is_empty(), "leaf must have empty dependencies");
}
#[test]
fn forest_nodes_covers_same_items_as_render_forest() {
let items = vec![
item(ItemKind::Skill, "a", "s"), item(ItemKind::Skill, "b", "s"), item(ItemKind::Skill, "c", "s"), item(ItemKind::Skill, "d", "s"), ];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
content.insert("skill:c".into(), "{{ns:d}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
installed_keys.insert("skill:d".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
fn collect_keys(nodes: &[DepNode], out: &mut HashSet<String>) {
for n in nodes {
out.insert(n.key.clone());
if let Some(children) = &n.dependencies {
collect_keys(children, out);
}
}
}
let nodes = g.forest_nodes();
let mut node_keys: HashSet<String> = HashSet::new();
collect_keys(&nodes, &mut node_keys);
for key in &installed_keys {
assert!(
node_keys.contains(key),
"forest_nodes must include installed key {key}: {nodes:?}"
);
}
let forest = g.render_forest();
for key in &installed_keys {
assert!(
forest.contains(key.as_str()),
"render_forest must contain key {key}: {forest:?}"
);
}
}
#[test]
fn forest_nodes_json_serialization_shape() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
let json_str = serde_json::to_string(&nodes).expect("must serialize");
let v: serde_json::Value = serde_json::from_str(&json_str).expect("must parse");
assert!(v.is_array());
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 1);
let root = &arr[0];
assert_eq!(root["key"], "skill:a");
assert!(
root.get("dependencies").is_some(),
"normal node must have dependencies"
);
assert!(
root.get("cycle").is_none(),
"normal node must not have cycle field"
);
let b_node = &root["dependencies"][0];
assert_eq!(b_node["key"], "skill:b");
assert!(b_node.get("cycle").is_none());
assert!(b_node.get("dependencies").is_some());
let cycle_leaf = &b_node["dependencies"][0];
assert_eq!(cycle_leaf["key"], "skill:a");
assert_eq!(cycle_leaf["cycle"], true);
assert!(
cycle_leaf.get("dependencies").is_none(),
"cycle leaf must not have dependencies field"
);
}
#[test]
fn forest_nodes_prefixed_items_use_effective_keys() {
let mut a = item(ItemKind::Skill, "a", "s");
a.prefix = Some("jk".to_string());
let mut b = item(ItemKind::Skill, "b", "s");
b.prefix = Some("jk".to_string());
let items = vec![a, b];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:jk:a".to_string());
installed_keys.insert("skill:jk:b".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].key, "skill:jk:a");
let children = nodes[0].dependencies.as_ref().unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].key, "skill:jk:b");
}
#[test]
fn forest_nodes_diamond_nests_shared_dep_under_both_dependents() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
item(ItemKind::Skill, "d", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}} {{ns:c}}".into());
content.insert("skill:b".into(), "{{ns:d}}".into());
content.insert("skill:c".into(), "{{ns:d}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
installed_keys.insert("skill:d".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
assert_eq!(nodes.len(), 1, "a must be the only root: {nodes:?}");
let a = &nodes[0];
assert_eq!(a.key, "skill:a");
assert!(!a.cycle);
let a_children = a.dependencies.as_ref().expect("a has deps");
assert_eq!(
a_children
.iter()
.map(|n| n.key.as_str())
.collect::<Vec<_>>(),
vec!["skill:b", "skill:c"],
"a's children must be b then c in discovery order"
);
let b_child = &a_children[0];
assert_eq!(&b_child.key, "skill:b");
let b_children = b_child.dependencies.as_ref().expect("b has deps");
assert_eq!(b_children.len(), 1, "b must have exactly one child");
let d_under_b = &b_children[0];
assert_eq!(d_under_b.key, "skill:d", "b's child must be d");
assert!(!d_under_b.cycle, "d under b must not be a cycle leaf");
assert!(!d_under_b.seen, "d's FIRST occurrence must not be `seen`");
assert!(
d_under_b
.dependencies
.as_ref()
.is_some_and(|v| v.is_empty()),
"d under b is a fully expanded leaf: empty dependencies, not absent"
);
let c_child = &a_children[1];
assert_eq!(&c_child.key, "skill:c");
let c_children = c_child.dependencies.as_ref().expect("c has deps");
assert_eq!(c_children.len(), 1, "c must have exactly one child");
let d_under_c = &c_children[0];
assert_eq!(d_under_c.key, "skill:d", "c's child must be d");
assert!(!d_under_c.cycle, "d under c must not be a cycle leaf");
assert!(
d_under_c.seen,
"d's SECOND occurrence (under c) must be a DEP-64 `seen` leaf"
);
assert!(
d_under_c.dependencies.is_none(),
"a `seen` leaf must have no dependencies field"
);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&nodes).unwrap()).unwrap();
assert_eq!(v[0]["key"], "skill:a");
assert_eq!(v[0]["dependencies"][0]["key"], "skill:b");
assert_eq!(v[0]["dependencies"][0]["dependencies"][0]["key"], "skill:d");
assert!(
v[0]["dependencies"][0]["dependencies"][0]
.get("seen")
.is_none()
);
assert_eq!(v[0]["dependencies"][1]["key"], "skill:c");
assert_eq!(v[0]["dependencies"][1]["dependencies"][0]["key"], "skill:d");
assert_eq!(v[0]["dependencies"][1]["dependencies"][0]["seen"], true);
let forest = g.render_forest();
assert_eq!(
forest.matches("- skill:d").count(),
2,
"human forest shows d twice (under b in full, under c as (seen)): {forest:?}"
);
assert!(
forest.contains("- skill:d (seen)"),
"the second occurrence must be marked (seen): {forest:?}"
);
}
#[test]
fn forest_nodes_independent_root_plus_cycle_component_all_items_present() {
let items = vec![
item(ItemKind::Skill, "a", "s"),
item(ItemKind::Skill, "b", "s"),
item(ItemKind::Skill, "c", "s"),
];
let mut content = HashMap::new();
content.insert("skill:a".into(), "{{ns:b}}".into());
content.insert("skill:b".into(), "{{ns:a}}".into());
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:a".to_string());
installed_keys.insert("skill:b".to_string());
installed_keys.insert("skill:c".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
assert_eq!(
nodes.iter().map(|n| n.key.as_str()).collect::<Vec<_>>(),
vec!["skill:c", "skill:a"],
"natural root c first, then promoted cycle root a: {nodes:?}"
);
assert!(nodes[0].dependencies.as_ref().is_some_and(|v| v.is_empty()));
let a = &nodes[1];
let b = &a.dependencies.as_ref().unwrap()[0];
assert_eq!(b.key, "skill:b");
assert!(!b.cycle);
let back = &b.dependencies.as_ref().unwrap()[0];
assert_eq!(back.key, "skill:a");
assert!(back.cycle, "the b->a back-edge must be a cycle leaf");
assert!(
back.dependencies.is_none(),
"cycle leaf has no dependencies"
);
fn collect(nodes: &[DepNode], out: &mut HashSet<String>) {
for n in nodes {
out.insert(n.key.clone());
if let Some(c) = &n.dependencies {
collect(c, out);
}
}
}
let mut seen = HashSet::new();
collect(&nodes, &mut seen);
for k in &installed_keys {
assert!(
seen.contains(k),
"every installed item must appear: missing {k}"
);
}
}
#[test]
fn direct_dependency_keys_ambiguous_bare_requires_emits_no_key() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["shared".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let read = reader(HashMap::new());
let keys = direct_dependency_keys(&items[0], &items, &read);
assert!(
keys.is_empty(),
"ambiguous bare requires must yield no adjacency key: {keys:?}"
);
}
#[test]
fn direct_dependency_keys_unique_bare_requires_emits_single_key() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["only".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "only", "s"),
item(ItemKind::Agent, "other", "s"),
];
let read = reader(HashMap::new());
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(
keys,
vec!["agent:only".to_string()],
"unique bare requires must resolve to exactly one key: {keys:?}"
);
}
#[test]
fn direct_dependency_keys_kind_qualified_requires_beats_same_named_other_kind() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["rule:shared".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let read = reader(HashMap::new());
let keys = direct_dependency_keys(&items[0], &items, &read);
assert_eq!(
keys,
vec!["rule:shared".to_string()],
"kind-qualified requires must pick exactly that kind: {keys:?}"
);
}
#[test]
fn installed_graph_ambiguous_bare_requires_yields_no_edge() {
let mut root = item(ItemKind::Skill, "root", "s");
root.requires = vec!["shared".to_string()];
let items = vec![
root,
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:root".to_string());
installed_keys.insert("agent:shared".to_string());
installed_keys.insert("rule:shared".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
assert!(
g.dependents("agent:shared").is_empty(),
"ambiguous bare requires must not edge to agent:shared: {:?}",
g.dependents("agent:shared")
);
assert!(
g.dependents("rule:shared").is_empty(),
"ambiguous bare requires must not edge to rule:shared: {:?}",
g.dependents("rule:shared")
);
let forest = g.render_forest();
assert_eq!(
forest, "- skill:root\n- agent:shared\n- rule:shared\n",
"ambiguous bare requires produces a flat forest (no edges): {forest:?}"
);
}
#[test]
fn installed_graph_unique_bare_requires_yields_one_edge() {
let mut root = item(ItemKind::Skill, "root", "s");
root.requires = vec!["only".to_string()];
let items = vec![root, item(ItemKind::Agent, "only", "s")];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:root".to_string());
installed_keys.insert("agent:only".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
assert_eq!(
g.dependents("agent:only"),
vec!["skill:root".to_string()],
"unique bare requires must form one edge in the installed graph"
);
assert_eq!(
g.render_forest(),
"- skill:root\n - agent:only\n",
"unique bare requires nests the single dep under root"
);
}
#[test]
fn token_and_requires_ambiguity_split_proven_in_one_scenario() {
let mut skill = item(ItemKind::Skill, "root", "s");
skill.requires = vec!["shared".to_string()];
let items = vec![
skill,
item(ItemKind::Agent, "shared", "s"),
item(ItemKind::Rule, "shared", "s"),
];
let mut content = HashMap::new();
content.insert("skill:root".into(), "{{ns:shared}}".into());
let read = reader(content.clone());
let mut keys = direct_dependency_keys(&items[0], &items, &read);
keys.sort();
assert_eq!(
keys,
vec!["agent:shared".to_string(), "rule:shared".to_string()],
"token fans out to both kinds; ambiguous requires adds nothing: {keys:?}"
);
let r = resolve(&items, &[0], &no_installed(), reader(content));
let order = r.install_order().to_vec();
assert_eq!(
order.len(),
3,
"only the two token deps plus root: {order:?}"
);
assert!(
order.contains(&1) && order.contains(&2),
"token must pull both kinds (DEP-3): {order:?}"
);
assert_eq!(*order.last().unwrap(), 0, "root installs last: {order:?}");
}
#[test]
fn installed_graph_prefixed_ambiguous_bare_requires_yields_no_edge() {
let mut root = item(ItemKind::Skill, "root", "s");
root.prefix = Some("jk".to_string());
root.requires = vec!["shared".to_string()];
let mut a = item(ItemKind::Agent, "shared", "s");
a.prefix = Some("jk".to_string());
let mut ru = item(ItemKind::Rule, "shared", "s");
ru.prefix = Some("jk".to_string());
let items = vec![root, a, ru];
let mut installed_keys = HashSet::new();
installed_keys.insert("skill:jk:root".to_string());
installed_keys.insert("agent:jk:shared".to_string());
installed_keys.insert("rule:jk:shared".to_string());
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
assert!(
g.dependents("agent:jk:shared").is_empty() && g.dependents("rule:jk:shared").is_empty(),
"ambiguous bare requires must emit no edge even when prefixed"
);
assert_eq!(
g.render_forest(),
"- skill:jk:root\n- agent:jk:shared\n- rule:jk:shared\n",
"prefixed ambiguous requires yields a flat (edgeless) forest"
);
}
#[test]
fn forest_nodes_empty_graph_is_empty_array() {
let items: Vec<CatalogItem> = vec![];
let installed_keys: HashSet<String> = HashSet::new();
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(HashMap::new()),
);
let nodes = g.forest_nodes();
assert!(nodes.is_empty(), "empty graph yields no roots: {nodes:?}");
assert_eq!(
serde_json::to_string(&nodes).unwrap(),
"[]",
"empty forest must serialize to a JSON empty array"
);
}
fn diamond_chain(levels: usize) -> (Vec<CatalogItem>, HashMap<String, String>) {
let mut items = Vec::new();
let mut content = HashMap::new();
for i in 0..levels {
for name in [format!("a{i}"), format!("b{i}"), format!("c{i}")] {
items.push(item(ItemKind::Skill, &name, "s"));
}
content.insert(
format!("skill:a{i}"),
format!(
"{} {}",
ns_token(&format!("b{i}")),
ns_token(&format!("c{i}"))
),
);
if i + 1 < levels {
content.insert(format!("skill:b{i}"), ns_token(&format!("a{}", i + 1)));
content.insert(format!("skill:c{i}"), ns_token(&format!("a{}", i + 1)));
}
}
(items, content)
}
fn plain_chain(len: usize) -> (Vec<CatalogItem>, HashMap<String, String>) {
let mut items = Vec::new();
let mut content = HashMap::new();
for i in 0..len {
let name = format!("n{i}");
items.push(item(ItemKind::Skill, &name, "s"));
if i + 1 < len {
content.insert(format!("skill:{name}"), ns_token(&format!("n{}", i + 1)));
}
}
(items, content)
}
#[test]
fn diamond_chain_render_tree_is_linear_not_exponential() {
const LEVELS: usize = 20;
let (items, content) = diamond_chain(LEVELS);
let total_nodes = items.len();
let r = resolve(&items, &[0], &no_installed(), reader(content));
let tree = r.render_tree(&items);
let line_count = tree.lines().count();
assert!(
line_count <= total_nodes * 4,
"rendered tree must be linear in node count, not exponential: \
{line_count} lines for {total_nodes} nodes"
);
assert!(
tree.contains("(seen)"),
"a diamond of this shape must produce at least one (seen) leaf: {tree:?}"
);
}
#[test]
fn deep_chain_render_tree_no_stack_overflow_with_truncation_notice() {
const CHAIN_LEN: usize = 5_000;
let (items, content) = plain_chain(CHAIN_LEN);
let r = resolve(&items, &[0], &no_installed(), reader(content));
let tree = r.render_tree(&items);
assert!(
tree.contains("(truncated: max depth reached)"),
"a chain past the depth cap must end in an explicit truncation notice: {:?}",
&tree[tree.len().saturating_sub(200)..]
);
let line_count = tree.lines().count();
assert!(
line_count < CHAIN_LEN / 2,
"rendering must stop well short of the full chain length: \
{line_count} lines for a {CHAIN_LEN}-node chain"
);
}
#[test]
fn diamond_chain_forest_is_linear_not_exponential() {
const LEVELS: usize = 20;
let (items, content) = diamond_chain(LEVELS);
let total_nodes = items.len();
let installed_keys: HashSet<String> = items
.iter()
.map(|it| it.key().as_str().to_string())
.collect();
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let forest = g.render_forest();
let line_count = forest.lines().count();
assert!(
line_count <= total_nodes * 4,
"installed forest must be linear in node count, not exponential: \
{line_count} lines for {total_nodes} nodes"
);
assert!(
forest.contains("(seen)"),
"a diamond of this shape must produce at least one (seen) leaf: {forest:?}"
);
}
#[test]
fn deep_chain_forest_no_stack_overflow_with_truncation_notice() {
const CHAIN_LEN: usize = 5_000;
let (items, content) = plain_chain(CHAIN_LEN);
let installed_keys: HashSet<String> = items
.iter()
.map(|it| it.key().as_str().to_string())
.collect();
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let forest = g.render_forest();
assert!(
forest.contains("(truncated: max depth reached)"),
"a chain past the depth cap must end in an explicit truncation notice"
);
let line_count = forest.lines().count();
assert!(
line_count < CHAIN_LEN / 2,
"rendering must stop well short of the full chain length: \
{line_count} lines for a {CHAIN_LEN}-node chain"
);
}
#[test]
fn diamond_chain_forest_nodes_is_linear_not_exponential() {
const LEVELS: usize = 20;
let (items, content) = diamond_chain(LEVELS);
let total_nodes = items.len();
let installed_keys: HashSet<String> = items
.iter()
.map(|it| it.key().as_str().to_string())
.collect();
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
fn count_nodes(nodes: &[DepNode]) -> usize {
nodes
.iter()
.map(|n| 1 + n.dependencies.as_deref().map(count_nodes).unwrap_or(0))
.sum()
}
fn has_seen(nodes: &[DepNode]) -> bool {
nodes
.iter()
.any(|n| n.seen || n.dependencies.as_deref().is_some_and(has_seen))
}
let node_count = count_nodes(&nodes);
assert!(
node_count <= total_nodes * 4,
"structured forest must be linear in node count, not exponential: \
{node_count} DepNode instances for {total_nodes} catalog nodes"
);
assert!(
has_seen(&nodes),
"a diamond of this shape must produce at least one seen leaf"
);
}
#[test]
fn deep_chain_forest_nodes_truncates_without_stack_overflow() {
const CHAIN_LEN: usize = 5_000;
let (items, content) = plain_chain(CHAIN_LEN);
let installed_keys: HashSet<String> = items
.iter()
.map(|it| it.key().as_str().to_string())
.collect();
let g = installed_graph(
&items,
|it| installed_keys.contains(it.key().as_str()),
reader(content),
);
let nodes = g.forest_nodes();
assert_eq!(nodes.len(), 1, "a plain chain has exactly one root");
let mut cur = &nodes[0];
let mut depth = 0usize;
loop {
let children = cur
.dependencies
.as_ref()
.expect("a non-truncated, non-leaf node has a dependencies field");
assert!(
!children.is_empty(),
"chain ended without a truncation marker at depth {depth}"
);
if children[0].key.contains("truncated") {
break;
}
cur = &children[0];
depth += 1;
assert!(
depth < CHAIN_LEN / 2,
"must truncate well before the full chain depth (stuck at depth {depth})"
);
}
}
}