use crate::brain::tools::Tool;
use crate::brain::tools::ToolExecutionContext;
use crate::brain::tools::load_brain_file::*;
use crate::brain::tools::seen_skills;
use crate::brain::tools::seen_skills::*;
use crate::db::Database;
use crate::db::repository::SessionSkillsRepository;
use std::path::Path;
use uuid::Uuid;
fn ctx() -> ToolExecutionContext {
ToolExecutionContext::new(Uuid::new_v4())
}
fn tool() -> LoadBrainFileTool {
LoadBrainFileTool
}
#[tokio::test]
async fn slug_form_loads_builtin_skill_body() {
let c = ctx();
let result = tool()
.execute(serde_json::json!({"name": "cost-estimate"}), &c)
.await
.unwrap();
assert!(result.success, "slug form of a built-in skill must succeed");
let text = result.output;
assert!(
text.contains("--- skill: cost-estimate ---"),
"body must be framed as a skill, got: {}",
&text[..text.len().min(200)]
);
assert!(!text.is_empty(), "skill body must not be empty");
}
#[tokio::test]
async fn slug_form_with_query_returns_matching_sections() {
let c = ctx();
let result = tool()
.execute(
serde_json::json!({"name": "cost-estimate", "query": "usage"}),
&c,
)
.await
.unwrap();
assert!(result.success, "slug+query form must succeed");
}
#[tokio::test]
async fn path_traversal_still_refused_after_slug_form_added() {
for bad in [
"../../etc/passwd",
"sub/file.md",
"../skills/cost-estimate/SKILL.md",
] {
let result = tool()
.execute(serde_json::json!({"name": bad}), &ctx())
.await
.unwrap();
assert!(!result.success, "traversal input {bad} must fail");
}
}
#[tokio::test]
async fn unknown_slug_falls_through_to_brain_file_handling() {
let result = tool()
.execute(
serde_json::json!({"name": "no-such-skill-or-brain"}),
&ctx(),
)
.await
.unwrap();
let body = result.output;
assert!(
body.contains("not found"),
"unknown slug must fall through to the brain-file miss, got: {body}"
);
assert!(
!body.contains("--- skill:"),
"unknown slug must never be answered with a skill body, got: {body}"
);
}
#[tokio::test]
async fn slug_load_marks_skill_seen_for_session() {
let c = ctx();
let session = c.session_id;
assert!(
!seen_skills::was_seen(session, "cost-estimate"),
"fresh session must not have the skill seen"
);
tool()
.execute(serde_json::json!({"name": "cost-estimate"}), &c)
.await
.unwrap();
assert!(
seen_skills::was_seen(session, "cost-estimate"),
"slug-load must mark the skill seen"
);
assert_eq!(
seen_skills::seen_for_session(session),
vec!["cost-estimate".to_string()]
);
}
#[test]
fn read_file_whole_read_marks_skill_seen_via_hook() {
let session = Uuid::new_v4();
let path = std::path::Path::new("/root/.opencrabs/profiles/ops/skills/grafana/SKILL.md");
let slug = seen_skills::skill_slug_from_path(path).expect("skill path must yield slug");
assert_eq!(slug, "grafana");
seen_skills::mark_seen(session, &slug);
assert!(seen_skills::was_seen(session, "grafana"));
}
#[test]
fn stamp_union_dedupes_active_and_seen() {
let mut active = std::collections::BTreeSet::new();
active.insert("opencrabs-dev".to_string());
let mut seen = std::collections::BTreeSet::new();
seen.insert("opencrabs-dev".to_string());
seen.insert("grafana".to_string());
let union: Vec<String> = active.union(&seen).cloned().collect();
assert_eq!(union.len(), 2, "overlap must dedupe");
assert_eq!(
union,
vec!["grafana".to_string(), "opencrabs-dev".to_string()]
);
}
#[test]
fn slug_extraction_from_skill_paths() {
assert_eq!(
seen_skills::skill_slug_from_path(Path::new(
"/root/.opencrabs/profiles/ops/skills/opencrabs-dev/SKILL.md"
)),
Some("opencrabs-dev".to_string())
);
assert_eq!(
seen_skills::skill_slug_from_path(Path::new("skills/foo/SKILL.md")),
Some("foo".to_string())
);
}
#[test]
fn non_skill_paths_yield_none() {
assert_eq!(
seen_skills::skill_slug_from_path(Path::new("/home/user/MEMORY.md")),
None
);
assert_eq!(
seen_skills::skill_slug_from_path(Path::new("skills/foo/other.md")),
None
);
assert_eq!(
seen_skills::skill_slug_from_path(Path::new("not-skills/foo/SKILL.md")),
None
);
assert_eq!(
seen_skills::skill_slug_from_path(Path::new("skills/foo/")),
None
);
}
#[test]
fn mark_seen_is_idempotent_and_session_scoped() {
let a = Uuid::new_v4();
let b = Uuid::new_v4();
seen_skills::mark_seen(a, "opencrabs-dev");
seen_skills::mark_seen(a, "opencrabs-dev");
assert!(seen_skills::was_seen(a, "opencrabs-dev"));
assert_eq!(
seen_skills::seen_for_session(a),
vec!["opencrabs-dev".to_string()]
);
assert!(!seen_skills::was_seen(b, "opencrabs-dev"));
assert!(seen_skills::seen_for_session(b).is_empty());
}
#[test]
fn seen_list_is_sorted_and_multi() {
let a = Uuid::new_v4();
seen_skills::mark_seen(a, "zeta");
seen_skills::mark_seen(a, "alpha");
assert_eq!(
seen_skills::seen_for_session(a),
vec!["alpha".to_string(), "zeta".to_string()]
);
}
async fn test_db() -> Database {
let db = Database::connect_in_memory()
.await
.expect("in-memory DB should connect");
db.run_migrations().await.expect("migrations should apply");
db
}
async fn insert_session(db: &Database, id: Uuid) {
db.pool()
.get()
.await
.expect("pool connection")
.interact(move |conn| {
conn.execute(
"INSERT INTO sessions (id, created_at, updated_at) VALUES (?1, ?2, ?3)",
rusqlite::params![id.to_string(), 0i64, 0i64],
)
})
.await
.expect("interact")
.expect("insert session");
}
#[tokio::test]
async fn filename_form_marks_skill_seen_for_session() {
let c = ctx();
let session = c.session_id;
let result = tool()
.execute(serde_json::json!({"name": "cost-estimate.md"}), &c)
.await
.unwrap();
assert!(
result.success,
"filename form of a built-in skill must succeed"
);
assert!(
seen_skills::was_seen(session, "cost-estimate"),
"filename-form load must mark the skill seen"
);
}
#[tokio::test]
async fn filename_form_with_query_marks_skill_seen_for_session() {
let c = ctx();
let session = c.session_id;
let result = tool()
.execute(
serde_json::json!({"name": "cost-estimate.md", "query": "estimate"}),
&c,
)
.await
.unwrap();
assert!(result.success);
assert!(
seen_skills::was_seen(session, "cost-estimate"),
"a query-filtered filename-form load must still register consumption"
);
}
#[tokio::test]
async fn record_and_read_round_trip() {
let db = test_db().await;
let repo = SessionSkillsRepository::new(db.pool().clone());
let sid = Uuid::new_v4();
repo.record(sid, "opencrabs-dev", 0).await.unwrap();
repo.record(sid, "cost-estimate", 2).await.unwrap();
let rows = repo.all().await.unwrap();
assert_eq!(rows.len(), 2);
let ce = rows
.iter()
.find(|r| r.1 == "cost-estimate")
.expect("cost-estimate row must round-trip");
let od = rows
.iter()
.find(|r| r.1 == "opencrabs-dev")
.expect("opencrabs-dev row must round-trip");
assert_eq!(ce.0, sid);
assert_eq!(ce.2, Some(2));
assert_eq!(od.0, sid);
assert_eq!(od.2, Some(0));
}
#[tokio::test]
async fn record_upserts_epoch_instead_of_duplicating() {
let db = test_db().await;
let repo = SessionSkillsRepository::new(db.pool().clone());
let sid = Uuid::new_v4();
repo.record(sid, "sk", 0).await.unwrap();
repo.record(sid, "sk", 3).await.unwrap();
let rows = repo.all().await.unwrap();
assert_eq!(rows.len(), 1, "same (session, slug) must upsert");
assert_eq!(
rows[0].2,
Some(3),
"the newer epoch must win — a stale row would re-gate the skill (#150)"
);
}
#[tokio::test]
async fn restart_hydrates_registry_from_persisted_rows() {
let db = test_db().await;
let repo = SessionSkillsRepository::new(db.pool().clone());
let sid = Uuid::new_v4();
repo.record(sid, "opencrabs-dev", 1).await.unwrap();
let rows = repo.all().await.unwrap();
seen_skills::apply_seeds(seen_skills::hydrate_from_rows(rows));
assert!(seen_skills::was_seen(sid, "opencrabs-dev"));
assert_eq!(
seen_skills::seen_for_session(sid),
vec!["opencrabs-dev".to_string()],
"the #125 stamp inventory must list a pre-restart skill"
);
assert!(
seen_skills::seen_since_compaction(sid, "opencrabs-dev"),
"the restored epoch counter must keep the skill in context"
);
}
#[tokio::test]
async fn prune_drops_orphans_and_keeps_live_sessions() {
let db = test_db().await;
let repo = SessionSkillsRepository::new(db.pool().clone());
let live = Uuid::new_v4();
let dead = Uuid::new_v4();
insert_session(&db, live).await;
repo.record(live, "sk", 0).await.unwrap();
repo.record(dead, "sk", 0).await.unwrap();
let pruned = repo.prune_missing_sessions().await.unwrap();
assert_eq!(pruned, 1, "only the orphaned row is dropped");
let rows = repo.all().await.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, live, "a live session's rows must survive");
}
#[test]
fn fresh_session_reports_not_seen_since_compaction() {
let a = Uuid::new_v4();
assert!(!seen_since_compaction(a, "anything"));
}
#[test]
fn seen_passes_and_compaction_rearms_gate() {
let a = Uuid::new_v4();
mark_seen(a, "my-skill");
assert!(seen_since_compaction(a, "my-skill"));
note_compaction(a);
assert!(!seen_since_compaction(a, "my-skill"));
mark_seen(a, "my-skill");
assert!(seen_since_compaction(a, "my-skill"));
}
#[test]
fn compaction_clears_nothing_stamp_inventory_intact() {
let a = Uuid::new_v4();
mark_seen(a, "one");
mark_seen(a, "two");
note_compaction(a);
assert_eq!(
seen_for_session(a),
vec!["one".to_string(), "two".to_string()]
);
assert!(!seen_since_compaction(a, "one"));
assert!(!seen_since_compaction(a, "two"));
}
#[test]
fn sessions_are_independent() {
let a = Uuid::new_v4();
let b = Uuid::new_v4();
mark_seen(a, "sk");
note_compaction(a);
mark_seen(b, "sk");
assert!(seen_since_compaction(b, "sk"));
assert!(!seen_since_compaction(a, "sk"));
}
#[test]
fn hydration_takes_max_epoch_per_session() {
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let seeds = hydrate_from_rows(vec![
(a, "one".to_string(), Some(1)),
(a, "two".to_string(), Some(3)),
(b, "one".to_string(), Some(1)),
]);
assert_eq!(seeds.seen.len(), 3);
assert_eq!(seeds.epochs.get(&a), Some(&3));
assert_eq!(seeds.epochs.get(&b), Some(&1));
}
#[test]
fn hydration_restores_epoch_counter_so_next_compaction_gates() {
let a = Uuid::new_v4();
apply_seeds(hydrate_from_rows(vec![(a, "sk".to_string(), Some(2))]));
assert!(was_seen(a, "sk"));
assert!(seen_since_compaction(a, "sk"));
note_compaction(a);
assert!(!seen_since_compaction(a, "sk"));
}
#[test]
fn hydration_null_epoch_reads_as_zero_and_stays_permissive() {
let a = Uuid::new_v4();
apply_seeds(hydrate_from_rows(vec![(a, "legacy".to_string(), None)]));
assert!(was_seen(a, "legacy"));
assert!(seen_since_compaction(a, "legacy"));
let b = Uuid::new_v4();
apply_seeds(hydrate_from_rows(vec![(b, "odd".to_string(), Some(-5))]));
assert!(seen_since_compaction(b, "odd"));
}
#[test]
fn hydration_preserves_stamp_inventory_across_restart() {
let a = Uuid::new_v4();
apply_seeds(hydrate_from_rows(vec![
(a, "zeta".to_string(), Some(0)),
(a, "alpha".to_string(), Some(1)),
]));
assert_eq!(
seen_for_session(a),
vec!["alpha".to_string(), "zeta".to_string()]
);
}
#[test]
fn boot_hydration_claims_its_flag_after_the_pool_guard() {
let src = include_str!("../brain/tools/seen_skills.rs");
let body = src
.split("pub fn hydrate_from_db()")
.nth(1)
.expect("hydrate_from_db must exist");
let pool = body
.find("global_pool()")
.expect("hydrate_from_db must still guard on the global pool");
let claim = body
.find("HYDRATED.swap")
.expect("hydrate_from_db must still claim a once-flag");
assert!(
claim > pool,
"the once-flag is claimed at byte {claim}, before the pool guard at \
{pool}: a boot with no pool would burn the process's only hydration \
attempt and restart durability would be lost for the whole run"
);
}