use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use cgx_engine::{
build_skill_data, generate_agents_md, generate_skill, CommunityInfo, Edge, GraphDb, Node,
SkillData,
};
static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
fn temp_dir() -> PathBuf {
let count = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("cgx-skill-test-{}-{}", std::process::id(), count));
std::fs::create_dir_all(&dir).expect("failed to create test dir");
std::fs::write(dir.join("dummy.txt"), "test").expect("failed to write dummy file");
dir
}
fn make_node(
id: &str,
kind: &str,
name: &str,
path: &str,
language: &str,
churn: f64,
coupling: f64,
community: i64,
in_degree: i64,
out_degree: i64,
) -> Node {
Node {
id: id.to_string(),
kind: kind.to_string(),
name: name.to_string(),
path: path.to_string(),
line_start: 1,
line_end: 10,
language: language.to_string(),
churn,
coupling,
community,
in_degree,
out_degree,
exported: false,
is_dead_candidate: false,
dead_reason: None,
complexity: 0.0,
is_test_file: false,
test_count: 0,
is_tested: false,
}
}
fn seed_graph(db: &GraphDb) {
let nodes = vec![
make_node(
"fn:src/auth.ts:login",
"Function",
"login",
"src/auth.ts",
"typescript",
0.8,
0.5,
1,
2,
1,
),
make_node(
"cls:src/auth.ts:AuthService",
"Class",
"AuthService",
"src/auth.ts",
"typescript",
0.3,
0.7,
1,
1,
0,
),
make_node(
"fn:src/db.ts:query",
"Function",
"query",
"src/db.ts",
"typescript",
0.0,
0.2,
2,
0,
0,
),
make_node(
"file:src/auth.ts",
"File",
"src/auth.ts",
"src/auth.ts",
"typescript",
0.8,
0.7,
1,
0,
0,
),
make_node(
"file:src/db.ts",
"File",
"src/db.ts",
"src/db.ts",
"typescript",
0.2,
0.0,
2,
0,
0,
),
];
let edges = vec![
Edge {
id: "fn:src/auth.ts:login|CALLS|fn:src/db.ts:query".to_string(),
src: "fn:src/auth.ts:login".to_string(),
dst: "fn:src/db.ts:query".to_string(),
kind: "CALLS".to_string(),
weight: 1.0,
confidence: 1.0,
},
Edge {
id: "fn:src/auth.ts:login|CALLS|cls:src/auth.ts:AuthService".to_string(),
src: "fn:src/auth.ts:login".to_string(),
dst: "cls:src/auth.ts:AuthService".to_string(),
kind: "CALLS".to_string(),
weight: 0.5,
confidence: 0.9,
},
Edge {
id: "file:src/auth.ts|IMPORTS|file:src/db.ts".to_string(),
src: "file:src/auth.ts".to_string(),
dst: "file:src/db.ts".to_string(),
kind: "IMPORTS".to_string(),
weight: 1.0,
confidence: 1.0,
},
];
db.upsert_nodes(&nodes).expect("upsert nodes failed");
db.upsert_edges(&edges).expect("upsert edges failed");
db.update_in_out_degrees().expect("update degrees failed");
}
#[test]
fn test_build_skill_data_basic() {
let dir = temp_dir();
let db = GraphDb::open(&dir).expect("failed to open db");
seed_graph(&db);
let data = build_skill_data(&db).expect("build_skill_data failed");
assert!(data.node_count > 0, "node_count should be > 0");
assert!(data.edge_count > 0, "edge_count should be > 0");
assert!(data.function_count > 0, "function_count should be > 0");
assert!(data.file_count > 0, "file_count should be > 0");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_generate_skill_has_no_placeholders() {
let data = SkillData {
indexed_at: "2026-05-01T12:00:00Z".to_string(),
node_count: 42,
function_count: 10,
class_count: 5,
file_count: 8,
edge_count: 100,
language_breakdown: "TypeScript 100%".to_string(),
community_count: 2,
top_communities: vec![
CommunityInfo {
id: 1,
label: "auth".to_string(),
node_count: 20,
},
CommunityInfo {
id: 2,
label: "db".to_string(),
node_count: 22,
},
],
hotspots: vec![],
entry_points: vec![],
god_nodes: vec![],
dead_code_count: 0,
dead_code_high: 0,
};
let skill = generate_skill(&data);
let unfilled: Vec<_> = skill.match_indices("{{").collect();
assert!(
unfilled.is_empty(),
"skill should have no unfilled placeholders: {:?}",
unfilled
);
assert!(
skill.contains("## When to Use cgx"),
"missing 'When to Use cgx' section"
);
assert!(
skill.contains("## Trigger Patterns"),
"missing 'Trigger Patterns' section"
);
assert!(skill.contains("## Commands"), "missing 'Commands' section");
assert!(skill.contains("## Workflow"), "missing 'Workflow' section");
assert!(
skill.contains("## Token Budget"),
"missing 'Token Budget' section"
);
assert!(
skill.contains("## This Codebase"),
"missing 'This Codebase' section"
);
assert!(
skill.contains("cgx summary"),
"missing 'cgx summary' command"
);
assert!(
skill.contains("cgx query find"),
"missing 'cgx query find' command"
);
assert!(
skill.contains("cgx query blast-radius"),
"missing 'cgx query blast-radius' command"
);
assert!(
skill.contains("cgx hotspots"),
"missing 'cgx hotspots' command"
);
}
#[test]
fn test_generate_skill_has_stats() {
let data = SkillData {
indexed_at: "2026-05-01T12:00:00Z".to_string(),
node_count: 42,
function_count: 10,
class_count: 5,
file_count: 8,
edge_count: 100,
language_breakdown: "TypeScript 100%".to_string(),
community_count: 2,
top_communities: vec![],
hotspots: vec![],
entry_points: vec![],
god_nodes: vec![],
dead_code_count: 0,
dead_code_high: 0,
};
let skill = generate_skill(&data);
assert!(skill.contains("42"), "skill should contain node count");
assert!(skill.contains("100"), "skill should contain edge count");
assert!(
skill.contains("TypeScript 100%"),
"skill should contain language breakdown"
);
assert!(
skill.contains("2026-05-01T12:00:00Z"),
"skill should contain indexed_at"
);
}
#[test]
fn test_generate_agents_md_has_no_placeholders() {
let data = SkillData {
indexed_at: "2026-05-01T12:00:00Z".to_string(),
node_count: 42,
function_count: 10,
class_count: 5,
file_count: 8,
edge_count: 100,
language_breakdown: "TypeScript 100%".to_string(),
community_count: 2,
top_communities: vec![CommunityInfo {
id: 1,
label: "auth".to_string(),
node_count: 20,
}],
hotspots: vec![],
entry_points: vec![],
god_nodes: vec![],
dead_code_count: 0,
dead_code_high: 0,
};
let agents = generate_agents_md(&data);
let unfilled: Vec<_> = agents.match_indices("{{").collect();
assert!(
unfilled.is_empty(),
"agents md should have no unfilled placeholders: {:?}",
unfilled
);
assert!(agents.contains("## Overview"), "missing 'Overview' section");
assert!(
agents.contains("## Module Map"),
"missing 'Module Map' section"
);
assert!(agents.contains("## Hotspots"), "missing 'Hotspots' section");
assert!(
agents.contains("## Entry Points"),
"missing 'Entry Points' section"
);
assert!(
agents.contains("CGX_SKILL.md"),
"should mention CGX_SKILL.md"
);
}
#[test]
fn test_generate_skill_hotspots_section() {
let hotspot = make_node(
"file:src/auth.ts",
"File",
"src/auth.ts",
"src/auth.ts",
"typescript",
0.9,
0.8,
1,
5,
0,
);
let data = SkillData {
indexed_at: "2026-05-01T12:00:00Z".to_string(),
node_count: 10,
function_count: 3,
class_count: 1,
file_count: 2,
edge_count: 5,
language_breakdown: "TypeScript 100%".to_string(),
community_count: 1,
top_communities: vec![],
hotspots: vec![hotspot],
entry_points: vec![],
god_nodes: vec![],
dead_code_count: 0,
dead_code_high: 0,
};
let skill = generate_skill(&data);
assert!(skill.contains("### Hotspots"), "missing Hotspots section");
assert!(
skill.contains("src/auth.ts"),
"hotspots should mention auth.ts"
);
assert!(
skill.contains("0.90"),
"hotspots should contain churn value"
);
}
#[test]
fn test_generate_skill_entry_points_section() {
let entry = make_node(
"fn:src/main.ts:main",
"Function",
"main",
"src/main.ts",
"typescript",
0.0,
0.0,
1,
0,
3,
);
let data = SkillData {
indexed_at: "2026-05-01T12:00:00Z".to_string(),
node_count: 5,
function_count: 2,
class_count: 0,
file_count: 1,
edge_count: 3,
language_breakdown: "TypeScript 100%".to_string(),
community_count: 1,
top_communities: vec![],
hotspots: vec![],
entry_points: vec![entry],
god_nodes: vec![],
dead_code_count: 0,
dead_code_high: 0,
};
let skill = generate_skill(&data);
assert!(
skill.contains("### Entry Points"),
"missing Entry Points section"
);
assert!(skill.contains("main"), "entry points should mention main");
}
#[test]
fn test_generate_skill_god_nodes_section() {
let god = make_node(
"fn:src/db.ts:query",
"Function",
"query",
"src/db.ts",
"typescript",
0.0,
0.0,
1,
10,
0,
);
let data = SkillData {
indexed_at: "2026-05-01T12:00:00Z".to_string(),
node_count: 5,
function_count: 2,
class_count: 0,
file_count: 1,
edge_count: 10,
language_breakdown: "TypeScript 100%".to_string(),
community_count: 1,
top_communities: vec![],
hotspots: vec![],
entry_points: vec![],
god_nodes: vec![god],
dead_code_count: 0,
dead_code_high: 0,
};
let skill = generate_skill(&data);
assert!(
skill.contains("### Most Depended-On Nodes"),
"missing God Nodes section"
);
assert!(skill.contains("query"), "god nodes should mention query");
assert!(
skill.contains("10 callers"),
"god nodes should show caller count"
);
}