use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
static CWD_LOCK: Mutex<()> = Mutex::new(());
#[tokio::test]
async fn test_gpu_monitor_logic() {
let state = hematite::ui::gpu_monitor::GpuState::new();
let (used, total) = state.read();
assert_eq!(used, 0);
assert_eq!(total, 0);
assert_eq!(state.ratio(), 0.0);
assert_eq!(state.label(), "N/A");
state
.used_mib
.store(4096, std::sync::atomic::Ordering::Relaxed);
state
.total_mib
.store(8192, std::sync::atomic::Ordering::Relaxed);
assert_eq!(state.read(), (4096, 8192));
assert_eq!(state.ratio(), 0.5);
assert_eq!(state.label(), "4.0 GB / 8.0 GB");
}
#[tokio::test]
async fn test_git_monitor_initial_state() {
use hematite::agent::git_monitor::{GitRemoteStatus, GitState};
let state = GitState::new();
assert_eq!(state.status(), GitRemoteStatus::Unknown);
assert_eq!(state.label(), "UNKNOWN");
assert_eq!(state.url(), "None");
}
#[test]
fn test_workspace_profile_detects_rust_project_shape() {
use hematite::agent::workspace_profile::detect_workspace_profile;
let workspace = tempfile::tempdir().expect("temp workspace");
fs::create_dir_all(workspace.path().join("src")).expect("create src");
fs::create_dir_all(workspace.path().join("tests")).expect("create tests");
fs::create_dir_all(workspace.path().join(".github").join("workflows"))
.expect("create workflows");
fs::write(
workspace.path().join("Cargo.toml"),
"[package]\nname='sample'\nversion='0.1.0'\n",
)
.expect("write cargo");
let profile = detect_workspace_profile(workspace.path());
assert_eq!(profile.workspace_mode, "project");
assert_eq!(profile.primary_stack.as_deref(), Some("rust"));
assert!(profile.stack_signals.iter().any(|entry| entry == "rust"));
assert!(profile
.package_managers
.iter()
.any(|entry| entry == "cargo"));
assert!(profile.important_paths.iter().any(|entry| entry == "src"));
assert!(profile.important_paths.iter().any(|entry| entry == "tests"));
}
#[test]
fn test_workspace_profile_uses_workspace_verify_profile_and_writes_file() {
use hematite::agent::workspace_profile::{
ensure_workspace_profile, profile_prompt_block, profile_report, workspace_profile_path,
};
let workspace = tempfile::tempdir().expect("temp workspace");
fs::create_dir_all(workspace.path().join("src")).expect("create src");
fs::create_dir_all(workspace.path().join(".hematite")).expect("create hematite dir");
fs::write(
workspace.path().join("Cargo.toml"),
"[package]\nname='sample'\nversion='0.1.0'\n",
)
.expect("write cargo");
fs::write(
workspace.path().join(".hematite").join("settings.json"),
r#"{
"verify": {
"default_profile": "rust",
"profiles": {
"rust": {
"build": "cargo build",
"test": "cargo test"
}
}
}
}"#,
)
.expect("write settings");
let profile = ensure_workspace_profile(workspace.path()).expect("ensure profile");
assert_eq!(profile.verify_profile.as_deref(), Some("rust"));
assert_eq!(profile.build_hint.as_deref(), Some("cargo build"));
assert_eq!(profile.test_hint.as_deref(), Some("cargo test"));
assert!(
workspace_profile_path(workspace.path()).exists(),
"profile file should be written"
);
let prompt_block = profile_prompt_block(workspace.path()).expect("profile prompt");
assert!(prompt_block.contains("Verify profile: rust"));
assert!(prompt_block.contains("Build hint: cargo build"));
let report = profile_report(workspace.path());
assert!(report.contains("Workspace Profile"));
assert!(report.contains("Verify profile: rust"));
assert!(report.contains("Path:"));
}
#[tokio::test]
async fn test_task_file_parsing() {
let root = PathBuf::from(".");
let hematite_dir = root.join(".hematite");
if !hematite_dir.exists() {
fs::create_dir_all(&hematite_dir).unwrap();
}
let task_file = hematite_dir.join("TASK_TEST.md");
let mock_task = "# Objective: Implement Sovereign Diagnostics\n\n- [ ] Task 1";
fs::write(&task_file, mock_task).unwrap();
let content = fs::read_to_string(&task_file).unwrap_or_default();
let objective = content
.lines()
.find(|l| l.starts_with("# Objective:"))
.map(|l| l.replace("# Objective:", "").trim().to_string())
.unwrap_or_else(|| "Standby".to_string());
assert_eq!(objective, "Implement Sovereign Diagnostics");
fs::remove_file(task_file).ok();
}
#[test]
fn test_vein_bm25_index_and_search() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let doc = "fn authenticate(token: &str) -> bool {\n token == \"secret\"\n}\n\n\
fn logout(user: &str) {\n println!(\"Logging out {}\", user);\n}";
let chunks = vein
.index_document("src/auth.rs", 1_000_000, doc)
.expect("index");
assert!(!chunks.is_empty(), "should produce chunks");
let results = vein.search_bm25("authenticate", 5).expect("search");
assert!(!results.is_empty(), "BM25 should find 'authenticate'");
assert!(results[0].content.contains("authenticate"));
assert_eq!(vein.file_count(), 1);
let rechunks = vein
.index_document("src/auth.rs", 1_000_000, doc)
.expect("re-index");
assert!(rechunks.is_empty(), "unchanged file should not re-index");
}
#[test]
fn test_vein_reset_clears_index() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document("src/lib.rs", 1, "pub fn foo() {}")
.unwrap();
assert_eq!(vein.file_count(), 1);
vein.reset();
assert_eq!(vein.file_count(), 0);
assert_eq!(vein.embedded_chunk_count(), 0);
}
#[test]
fn test_vein_l1_no_heat_returns_none() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
assert!(vein.l1_context().is_none(), "no edits means no L1 block");
}
#[test]
fn test_vein_l1_bump_and_retrieve() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document(
"src/agent/conversation.rs",
1_000_000,
"pub fn run() {}\npub fn stop() {}\n",
)
.unwrap();
vein.bump_heat("src/agent/conversation.rs");
vein.bump_heat("src/agent/conversation.rs");
vein.bump_heat("src/agent/conversation.rs");
let l1 = vein.l1_context().expect("should have L1 after edits");
assert!(
l1.contains("src/agent/conversation.rs"),
"hot file should appear in L1"
);
assert!(l1.contains("3 edits"), "edit count should be 3");
}
#[test]
fn test_vein_l1_ranks_by_heat() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document("src/cold.rs", 1_000, "pub fn cold() {}")
.unwrap();
vein.index_document("src/hot.rs", 2_000, "pub fn hot() {}")
.unwrap();
vein.bump_heat("src/cold.rs");
vein.bump_heat("src/hot.rs");
vein.bump_heat("src/hot.rs");
vein.bump_heat("src/hot.rs");
let l1 = vein.l1_context().expect("L1 should exist");
let hot_pos = l1.find("src/hot.rs").unwrap_or(usize::MAX);
let cold_pos = l1.find("src/cold.rs").unwrap_or(usize::MAX);
assert!(hot_pos < cold_pos, "hotter file should appear first in L1");
}
#[test]
fn test_detect_room_known_segments() {
use hematite::memory::vein::detect_room;
assert_eq!(detect_room("src/agent/conversation.rs"), "agent");
assert_eq!(detect_room("src/ui/tui.rs"), "ui");
assert_eq!(detect_room("src/tools/file_ops.rs"), "tools");
assert_eq!(detect_room("src/memory/vein.rs"), "memory");
assert_eq!(detect_room("tests/diagnostics.rs"), "tests");
}
#[test]
fn test_detect_room_specialized_roles() {
use hematite::memory::vein::detect_room;
assert_eq!(detect_room("src/runtime.rs"), "runtime");
assert_eq!(detect_room("src/agent/mcp_manager.rs"), "integration");
assert_eq!(detect_room("Cargo.toml"), "config");
assert_eq!(detect_room("installer/hematite.iss"), "release");
assert_eq!(
detect_room(".github/workflows/windows-release.yml"),
"automation"
);
assert_eq!(detect_room("README.md"), "docs");
}
#[test]
fn test_detect_room_fallback() {
use hematite::memory::vein::detect_room;
assert_eq!(detect_room("src/plain.rs"), "src");
assert_eq!(detect_room("notes.bin"), "root");
}
#[test]
fn test_detect_room_session_prefix() {
use hematite::memory::vein::detect_room;
assert_eq!(
detect_room("session/2026-04-09/2026-04-09_20-15-00/turn-12"),
"session"
);
assert_eq!(
detect_room(".hematite/imports/claude-rollout.jsonl"),
"session"
);
}
#[test]
fn test_vein_l1_grouped_by_room() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document("src/agent/conversation.rs", 1_000, "pub fn run() {}")
.unwrap();
vein.index_document("src/ui/tui.rs", 2_000, "pub fn draw() {}")
.unwrap();
vein.bump_heat("src/agent/conversation.rs");
vein.bump_heat("src/ui/tui.rs");
let l1 = vein.l1_context().expect("L1 should exist");
assert!(l1.contains("[agent]"), "should have agent room header");
assert!(l1.contains("[ui]"), "should have ui room header");
}
#[test]
fn test_vein_inspection_snapshot_reports_counts_and_hot_files() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let docs_dir = workspace.path().join(".hematite").join("docs");
let reports_dir = workspace.path().join(".hematite").join("reports");
fs::create_dir_all(&docs_dir).expect("create docs dir");
fs::create_dir_all(&reports_dir).expect("create reports dir");
fs::write(
docs_dir.join("memory-notes.md"),
"# Notes\n\nopalvector reference doc\n",
)
.expect("write doc");
let report = serde_json::json!({
"session_start": "2026-04-10_09-30-00",
"transcript": [
{ "speaker": "You", "text": "remember opalvector?" },
{ "speaker": "Hematite", "text": "we kept the memory report operator-visible." }
]
});
fs::write(
reports_dir.join("session_2026-04-10_09-30-00.json"),
serde_json::to_string_pretty(&report).expect("serialize report"),
)
.expect("write report");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document("src/agent/conversation.rs", 1_000, "pub fn run_turn() {}")
.unwrap();
let indexed = vein.index_workspace_artifacts(workspace.path());
assert_eq!(indexed, 2, "should index one doc and one session exchange");
vein.bump_heat("src/agent/conversation.rs");
vein.bump_heat("src/agent/conversation.rs");
vein.bump_heat(".hematite/docs/memory-notes.md");
let snapshot = vein.inspect_snapshot(5);
assert_eq!(snapshot.indexed_source_files, 1);
assert_eq!(snapshot.indexed_docs, 1);
assert_eq!(snapshot.indexed_session_exchanges, 1);
assert_eq!(snapshot.embedded_source_doc_chunks, 0);
assert_eq!(snapshot.active_room.as_deref(), Some("agent"));
assert!(
snapshot.l1_ready,
"hot files should make the L1 block available"
);
assert_eq!(snapshot.hot_files.len(), 2);
assert_eq!(snapshot.hot_files[0].path, "src/agent/conversation.rs");
assert_eq!(snapshot.hot_files[0].room, "agent");
assert_eq!(snapshot.hot_files[0].heat, 2);
}
#[test]
fn test_vein_indexes_workspace_artifacts_without_project_source() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let docs_dir = workspace.path().join(".hematite").join("docs");
fs::create_dir_all(&docs_dir).expect("create docs dir");
fs::write(
docs_dir.join("reference.md"),
"# Operator Notes\n\nsunstonealpha docs-only retrieval survives outside projects.\n",
)
.expect("write docs");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_workspace_artifacts(workspace.path());
assert_eq!(indexed, 1, "should index the docs artifact");
let results = vein
.search_bm25("sunstonealpha retrieval", 5)
.expect("search docs");
assert!(!results.is_empty(), "docs artifact should be searchable");
assert_eq!(results[0].path, ".hematite/docs/reference.md");
assert_eq!(
vein.file_count(),
1,
"docs should count toward status files"
);
}
#[test]
fn test_vein_indexes_recent_session_reports_by_exchange_pair() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let reports_dir = workspace.path().join(".hematite").join("reports");
fs::create_dir_all(&reports_dir).expect("create reports dir");
let report = serde_json::json!({
"session_start": "2026-04-09_20-15-00",
"transcript": [
{ "speaker": "System", "text": "startup noise" },
{ "speaker": "You", "text": "Remember artifact obsidiankite?" },
{ "speaker": "Hematite", "text": "We decided to keep docs-only vein mode active outside projects." },
{ "speaker": "Tool", "text": "tool chatter" },
{ "speaker": "You", "text": "What about embercache?" },
{ "speaker": "Hematite", "text": "Session exchanges should be chunked per user plus assistant pair." }
]
});
fs::write(
reports_dir.join("session_2026-04-09_20-15-00.json"),
serde_json::to_string_pretty(&report).expect("serialize report"),
)
.expect("write report");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_recent_session_reports(workspace.path());
assert_eq!(indexed, 2, "two exchange pairs should be indexed");
let results = vein
.search_bm25("obsidiankite docs-only", 5)
.expect("search sessions");
assert!(!results.is_empty(), "session exchange should be searchable");
assert!(results[0].path.starts_with("session/2026-04-09/"));
assert_eq!(
vein.file_count(),
0,
"session chunks should not inflate status file counts"
);
assert_eq!(
vein.embedded_chunk_count(),
0,
"no embeddings were generated in the test"
);
}
#[test]
fn test_vein_session_report_caps_to_recent_sessions_and_turns() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let reports_dir = workspace.path().join(".hematite").join("reports");
fs::create_dir_all(&reports_dir).expect("create reports dir");
for day in 1..=6 {
let stamp = format!("2026-04-0{}_10-00-00", day);
let user_token = format!("sessiontoken{}", day);
let transcript = if day == 6 {
(1..=55)
.flat_map(|turn| {
[
serde_json::json!({
"speaker": "You",
"text": format!("turntoken{} request", turn),
}),
serde_json::json!({
"speaker": "Hematite",
"text": format!("turntoken{} response", turn),
}),
]
})
.collect::<Vec<_>>()
} else {
vec![
serde_json::json!({ "speaker": "You", "text": format!("{} request", user_token) }),
serde_json::json!({ "speaker": "Hematite", "text": format!("{} response", user_token) }),
]
};
let report = serde_json::json!({
"session_start": stamp,
"transcript": transcript,
});
fs::write(
reports_dir.join(format!("session_{}.json", stamp)),
serde_json::to_string_pretty(&report).expect("serialize report"),
)
.expect("write report");
}
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_recent_session_reports(workspace.path());
assert_eq!(
indexed, 54,
"last five sessions should be indexed with the newest session capped at 50 pairs"
);
let oldest = vein.search_bm25("sessiontoken1", 5).expect("search oldest");
assert!(
oldest.is_empty(),
"the oldest sixth session should be pruned"
);
let retained = vein
.search_bm25("sessiontoken2", 5)
.expect("search retained session");
assert!(
!retained.is_empty(),
"newer sessions within the five-session cap should remain searchable"
);
let early_turn = vein
.search_bm25("turntoken1", 5)
.expect("search early turn");
assert!(
early_turn.is_empty(),
"early turns beyond the 50-pair cap should be dropped"
);
let late_turn = vein
.search_bm25("turntoken55", 5)
.expect("search late turn");
assert!(
!late_turn.is_empty(),
"latest turns within the cap should remain searchable"
);
}
#[test]
fn test_vein_search_context_boosts_exact_phrases() {
use hematite::memory::vein::Vein;
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document(
"src/ui/startup.rs",
1,
"startup panel work startup panel work startup controls startup panel",
)
.expect("index startup");
vein.index_document(
"src/ui/specular.rs",
2,
"The specular panel shows the active context and event log.",
)
.expect("index specular");
let results = vein
.search_context("How does the \"specular panel\" work at startup?", 2)
.expect("search context");
assert_eq!(
results[0].path, "src/ui/specular.rs",
"exact quoted phrase should outrank generic token overlap"
);
}
#[test]
fn test_vein_search_context_boosts_standout_query_tokens() {
use hematite::memory::vein::Vein;
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document(
"src/release.rs",
1,
"installer flow local build docs tags portable build installer flow local build release command",
)
.expect("index generic release");
vein.index_document(
"src/tools/basalttrace.rs",
2,
"Basalttrace changed the release pipeline.",
)
.expect("index standout token");
let results = vein
.search_context(
"why did basalttrace installer flow change for local build",
2,
)
.expect("search context");
assert_eq!(
results[0].path, "src/tools/basalttrace.rs",
"standout repo/tool token should outrank generic overlap"
);
}
#[test]
fn test_vein_search_context_prefers_session_memory_for_historical_queries() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let docs_dir = workspace.path().join(".hematite").join("docs");
let reports_dir = workspace.path().join(".hematite").join("reports");
fs::create_dir_all(&docs_dir).expect("create docs dir");
fs::create_dir_all(&reports_dir).expect("create reports dir");
fs::write(
docs_dir.join("opalcache.md"),
"Opalcache docs-only mode keeps local support notes searchable.",
)
.expect("write doc");
let report = serde_json::json!({
"session_start": "2026-04-10_08-45-00",
"transcript": [
{ "speaker": "You", "text": "What should we do about opalcache docs-only mode?" },
{ "speaker": "Hematite", "text": "We decided earlier to keep session and import memory searchable outside project folders." }
]
});
fs::write(
reports_dir.join("session_2026-04-10_08-45-00.json"),
serde_json::to_string_pretty(&report).expect("serialize report"),
)
.expect("write report");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_workspace_artifacts(workspace.path());
assert_eq!(indexed, 2, "should index one doc and one session exchange");
let results = vein
.search_context(
"what did we decide earlier about opalcache docs-only mode?",
2,
)
.expect("search context");
assert!(
results[0].path.starts_with("session/"),
"historical decision query should prefer session memory"
);
}
#[test]
fn test_vein_search_context_biases_session_memory_by_explicit_date() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let reports_dir = workspace.path().join(".hematite").join("reports");
fs::create_dir_all(&reports_dir).expect("create reports dir");
let older_report = serde_json::json!({
"session_start": "2026-04-08_09-00-00",
"transcript": [
{ "speaker": "You", "text": "What should we do about quartzharbor docs-only rollout?" },
{ "speaker": "Hematite", "text": "On April 8 we delayed the quartzharbor docs-only rollout. Quartzharbor docs-only rollout delay remained the plan." }
]
});
fs::write(
reports_dir.join("session_2026-04-08_09-00-00.json"),
serde_json::to_string_pretty(&older_report).expect("serialize older report"),
)
.expect("write older report");
let newer_report = serde_json::json!({
"session_start": "2026-04-09_09-00-00",
"transcript": [
{ "speaker": "You", "text": "What should we do about quartzharbor docs-only rollout?" },
{ "speaker": "Hematite", "text": "On April 9 we decided to keep the quartzharbor docs-only rollout live." }
]
});
fs::write(
reports_dir.join("session_2026-04-09_09-00-00.json"),
serde_json::to_string_pretty(&newer_report).expect("serialize newer report"),
)
.expect("write newer report");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_recent_session_reports(workspace.path());
assert_eq!(indexed, 2, "two session exchanges should be indexed");
let results = vein
.search_context(
"what did we decide on 2026-04-09 about quartzharbor docs-only rollout?",
2,
)
.expect("search dated session context");
assert!(
results[0].path.starts_with("session/2026-04-09/"),
"explicit date query should favor the matching session date even when another session has heavier lexical overlap"
);
}
#[test]
fn test_vein_indexes_imported_marker_transcript_exchanges() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let imports_dir = workspace.path().join(".hematite").join("imports");
fs::create_dir_all(&imports_dir).expect("create imports dir");
fs::write(
imports_dir.join("handoff.txt"),
"> Remember emberforge and the release script?\nWe switched to a single release command.\n\n> What about docs-only mode?\nIt should still search imported chat exports.\n",
)
.expect("write transcript");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_imported_session_exports(workspace.path());
assert_eq!(indexed, 2, "two imported exchange pairs should be indexed");
let results = vein
.search_bm25("emberforge release command", 5)
.expect("search imported transcript");
assert!(
!results.is_empty(),
"imported transcript should be searchable"
);
assert!(results[0].path.starts_with("session/imports/"));
assert_eq!(
vein.file_count(),
0,
"imported session chunks should not inflate source/doc file counts"
);
}
#[test]
fn test_vein_indexes_imported_codex_jsonl_exchanges() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let imports_dir = workspace.path().join(".hematite").join("imports");
fs::create_dir_all(&imports_dir).expect("create imports dir");
let jsonl = r#"{"type":"session_meta","id":"abc"}
{"type":"event_msg","payload":{"type":"user_message","message":"Remember basalttrace and why we changed the installer flow?"}}
{"type":"event_msg","payload":{"type":"agent_message","message":"We wanted one release command to update tags, docs, and the local portable build."}}
{"type":"event_msg","payload":{"type":"user_message","message":"What should imports do?"}}
{"type":"event_msg","payload":{"type":"agent_message","message":"Imported chats should be searchable as session memory without polluting source counts."}}"#;
fs::write(imports_dir.join("codex-rollout.jsonl"), jsonl).expect("write jsonl");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_imported_session_exports(workspace.path());
assert_eq!(indexed, 2, "two codex exchange pairs should be indexed");
let results = vein
.search_bm25("basalttrace installer flow", 5)
.expect("search codex import");
assert!(!results.is_empty(), "codex import should be searchable");
assert!(
results[0].content.contains("Imported session exchange"),
"imported exchanges should be labeled as imported memory"
);
}
#[test]
fn test_vein_indexes_imported_claude_code_jsonl_exchanges() {
use hematite::memory::vein::Vein;
let workspace = tempfile::tempdir().expect("temp workspace");
let imports_dir = workspace.path().join(".hematite").join("imports");
fs::create_dir_all(&imports_dir).expect("create imports dir");
let jsonl = r#"{"type":"human","message":{"content":[{"type":"text","text":"Remember opalcache and the docs-only rule?"}]}}
{"type":"assistant","message":{"content":[{"type":"text","text":"We kept docs-only retrieval alive outside projects and made imported chats searchable too."}]}}"#;
fs::write(imports_dir.join("claude-code.jsonl"), jsonl).expect("write claude jsonl");
let db = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(db.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
let indexed = vein.index_imported_session_exports(workspace.path());
assert_eq!(
indexed, 1,
"one Claude Code exchange pair should be indexed"
);
let results = vein
.search_bm25("opalcache docs-only retrieval", 5)
.expect("search claude import");
assert!(
!results.is_empty(),
"Claude Code import should be searchable"
);
}
#[test]
fn test_extract_markdown_succeeds() {
use hematite::memory::vein::extract_document_text;
let mut tmp = tempfile::NamedTempFile::with_suffix(".md").expect("temp md");
writeln!(
tmp,
"# Design Doc\n\nThis is a specification for the auth module."
)
.unwrap();
let result = extract_document_text(tmp.path());
assert!(result.is_ok(), "markdown extraction should succeed");
assert!(result.unwrap().contains("Design Doc"));
}
#[test]
fn test_extract_txt_succeeds() {
use hematite::memory::vein::extract_document_text;
let mut tmp = tempfile::NamedTempFile::with_suffix(".txt").expect("temp txt");
writeln!(
tmp,
"API reference for the payment service.\n\nEndpoint: POST /charge"
)
.unwrap();
let result = extract_document_text(tmp.path());
assert!(result.is_ok());
assert!(result.unwrap().contains("payment service"));
}
#[test]
fn test_pdf_quality_guard_rejects_garbled_text() {
use hematite::memory::vein::extract_document_text;
let mut tmp = tempfile::NamedTempFile::with_suffix(".txt").expect("temp");
writeln!(
tmp,
"This is a well formatted document with proper spacing between all words."
)
.unwrap();
let result = extract_document_text(tmp.path());
assert!(result.is_ok());
}
#[tokio::test]
async fn test_inspect_host_directory_reports_counts_and_names() {
use serde_json::json;
let workspace = tempfile::tempdir().expect("temp workspace");
let nested = workspace.path().join("nested");
fs::create_dir_all(&nested).expect("create nested dir");
fs::write(workspace.path().join("alpha.txt"), "hematite").expect("write alpha");
fs::write(nested.join("beta.log"), "operator").expect("write beta");
let args = json!({
"topic": "directory",
"path": workspace.path().display().to_string(),
"max_entries": 5
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host directory");
assert!(output.contains("Directory inspection: Directory"));
assert!(output.contains("Top-level items: 2"));
assert!(output.contains("alpha.txt"));
assert!(output.contains("nested"));
assert!(output.contains("Recursive files: 2"));
}
#[tokio::test]
async fn test_inspect_host_path_reports_path_summary() {
use serde_json::json;
let args = json!({
"topic": "path",
"max_entries": 5
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host path");
assert!(output.contains("Host inspection: PATH"));
assert!(output.contains("Total entries:"));
assert!(output.contains("PATH entries:"));
}
#[tokio::test]
async fn test_inspect_host_processes_can_filter_current_binary() {
use serde_json::json;
let process_name = std::env::current_exe()
.expect("current exe")
.file_stem()
.expect("file stem")
.to_string_lossy()
.to_string();
let args = json!({
"topic": "processes",
"name": process_name,
"max_entries": 5
});
let output = match hematite::tools::host_inspect::inspect_host(&args).await {
Ok(output) => output,
Err(err)
if err.contains("Failed to run tasklist")
|| err.contains("tasklist returned a non-success status")
|| err.contains("Failed to run ps")
|| err.contains("ps returned a non-success status") =>
{
println!("Skipping processes test on this host: {}", err);
return;
}
Err(err) => panic!("inspect host processes failed: {}", err),
};
assert!(output.contains("Host inspection: processes"));
assert!(output.contains("Filter name:"));
assert!(output.contains("Processes found:"));
}
#[tokio::test]
async fn test_inspect_host_network_reports_adapter_summary() {
use serde_json::json;
let args = json!({
"topic": "network",
"max_entries": 5
});
let output = match hematite::tools::host_inspect::inspect_host(&args).await {
Ok(output) => output,
Err(err)
if err.contains("Failed to run ipconfig")
|| err.contains("ipconfig returned a non-success status")
|| err.contains("Failed to run ip addr")
|| err.contains("ip addr returned a non-success status")
|| err.contains("Failed to run ip route")
|| err.contains("ip route returned a non-success status") =>
{
println!("Skipping network test on this host: {}", err);
return;
}
Err(err) => panic!("inspect host network failed: {}", err),
};
assert!(output.contains("Host inspection: network"));
assert!(output.contains("Adapters found:"));
assert!(output.contains("Listener exposure:"));
assert!(output.contains("Adapter summary:"));
}
#[tokio::test]
async fn test_inspect_host_connectivity_returns_header() {
use serde_json::json;
let args = json!({ "topic": "connectivity" });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host connectivity should not hard-error");
assert!(
output.contains("Host inspection: connectivity"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_connectivity_reports_internet_status() {
use serde_json::json;
let args = json!({ "topic": "connectivity" });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host connectivity should not hard-error");
assert!(
output.contains("Internet:") || output.contains("internet"),
"expected internet status in output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_wifi_returns_header() {
use serde_json::json;
let args = json!({ "topic": "wifi" });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host wifi should not hard-error");
assert!(
output.contains("Host inspection: wifi"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_connections_returns_header() {
use serde_json::json;
let args = json!({ "topic": "connections", "max_entries": 10 });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host connections should not hard-error");
assert!(
output.contains("Host inspection: connections"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_vpn_returns_header() {
use serde_json::json;
let args = json!({ "topic": "vpn" });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host vpn should not hard-error");
assert!(
output.contains("Host inspection: vpn"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_proxy_returns_header() {
use serde_json::json;
let args = json!({ "topic": "proxy" });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host proxy should not hard-error");
assert!(
output.contains("Host inspection: proxy"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_firewall_rules_returns_header() {
use serde_json::json;
let args = json!({ "topic": "firewall_rules", "max_entries": 10 });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host firewall_rules should not hard-error");
assert!(
output.contains("Host inspection: firewall_rules"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_traceroute_returns_header() {
use serde_json::json;
let args = json!({ "topic": "traceroute", "host": "8.8.8.8", "max_entries": 10 });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host traceroute should not hard-error");
assert!(
output.contains("Host inspection: traceroute"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_dns_cache_returns_header() {
use serde_json::json;
let args = json!({ "topic": "dns_cache", "max_entries": 20 });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host dns_cache should not hard-error");
assert!(
output.contains("Host inspection: dns_cache"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_arp_returns_header() {
use serde_json::json;
let args = json!({ "topic": "arp" });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host arp should not hard-error");
assert!(
output.contains("Host inspection: arp"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_route_table_returns_header() {
use serde_json::json;
let args = json!({ "topic": "route_table", "max_entries": 20 });
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect_host route_table should not hard-error");
assert!(
output.contains("Host inspection: route_table"),
"unexpected output: {output}"
);
}
#[tokio::test]
async fn test_inspect_host_services_reports_status_summary() {
use serde_json::json;
let args = json!({
"topic": "services",
"max_entries": 5
});
let output = match hematite::tools::host_inspect::inspect_host(&args).await {
Ok(output) => output,
Err(err)
if err.contains("Failed to run PowerShell service inspection")
|| err.contains("PowerShell service inspection returned a non-success status")
|| err.contains("Failed to run systemctl list-units")
|| err.contains("systemctl list-units returned a non-success status")
|| err.contains("Failed to run systemctl list-unit-files")
|| err.contains("systemctl list-unit-files returned a non-success status") =>
{
println!("Skipping services test on this host: {}", err);
return;
}
Err(err) => panic!("inspect host services failed: {}", err),
};
assert!(output.contains("Host inspection: services"));
assert!(output.contains("Services found:"));
assert!(output.contains("Service summary:"));
}
#[tokio::test]
async fn test_inspect_host_env_doctor_reports_package_manager_health() {
use serde_json::json;
let args = json!({
"topic": "env_doctor",
"max_entries": 5
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host env doctor");
assert!(output.contains("Host inspection: env_doctor"));
assert!(output.contains("PATH health:"));
assert!(output.contains("Package managers found:"));
assert!(output.contains("Findings:"));
assert!(output.contains("Guidance:"));
}
#[tokio::test]
async fn test_inspect_host_fix_plan_for_path_reports_grounded_steps() {
use serde_json::json;
let args = json!({
"topic": "fix_plan",
"issue": "How do I fix cargo not found on this machine?"
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host fix plan env");
assert!(output.contains("Host inspection: fix_plan"));
assert!(output.contains("Fix-plan type: environment/path"));
assert!(output.contains("Fix plan:"));
assert!(output.contains("Why this works:"));
}
#[tokio::test]
async fn test_inspect_host_fix_plan_for_port_mentions_requested_port() {
use serde_json::json;
let args = json!({
"topic": "fix_plan",
"issue": "How do I fix port 3000 already in use?",
"port": 3000
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host fix plan port");
assert!(output.contains("Host inspection: fix_plan"));
assert!(output.contains("Fix-plan type: port_conflict"));
assert!(output.contains("Requested port: 3000"));
}
#[tokio::test]
async fn test_inspect_host_fix_plan_for_lm_studio_mentions_configured_endpoint() {
use serde_json::json;
let args = json!({
"topic": "fix_plan",
"issue": "How do I fix Hematite when LM Studio is not reachable on localhost:1234?"
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host fix plan lm studio");
assert!(output.contains("Host inspection: fix_plan"));
assert!(output.contains("Fix-plan type: lm_studio"));
assert!(output.contains("Configured API URL:"));
assert!(output.contains("Fix plan:"));
}
#[tokio::test]
async fn test_inspect_host_disk_reports_size_summary() {
use serde_json::json;
let workspace = tempfile::tempdir().expect("temp workspace");
let nested = workspace.path().join("nested");
fs::create_dir_all(&nested).expect("create nested dir");
fs::write(workspace.path().join("alpha.bin"), vec![0u8; 2048]).expect("write alpha");
fs::write(nested.join("beta.bin"), vec![0u8; 1024]).expect("write beta");
let args = json!({
"topic": "disk",
"path": workspace.path().display().to_string(),
"max_entries": 5
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host disk");
assert!(output.contains("Directory inspection: Disk"));
assert!(output.contains("Total size:"));
assert!(output.contains("Largest top-level entries:"));
}
#[tokio::test]
async fn test_inspect_host_repo_doctor_reports_workspace_state() {
use serde_json::json;
let workspace = tempfile::tempdir().expect("temp workspace");
fs::write(
workspace.path().join("Cargo.toml"),
"[package]\nname = \"sample\"\nversion = \"0.9.0\"\nedition = \"2021\"\n",
)
.expect("write cargo manifest");
fs::create_dir_all(workspace.path().join(".hematite").join("docs")).expect("docs dir");
fs::create_dir_all(workspace.path().join(".hematite").join("imports")).expect("imports dir");
fs::create_dir_all(workspace.path().join(".hematite").join("reports")).expect("reports dir");
fs::write(
workspace
.path()
.join(".hematite")
.join("workspace_profile.json"),
"{}",
)
.expect("write workspace profile");
let args = json!({
"topic": "repo_doctor",
"path": workspace.path().display().to_string(),
"max_entries": 5
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host repo doctor");
assert!(output.contains("Host inspection: repo_doctor"));
assert!(output.contains("Workspace mode: project"));
assert!(output.contains("Project markers:"));
assert!(output.contains("Cargo.toml"));
assert!(output.contains("Hematite docs/imports/reports: 0/0/0"));
assert!(output.contains("Workspace profile: present"));
assert!(output.contains("Cargo version: 0.9.0"));
}
#[tokio::test]
async fn test_inspect_host_ports_can_filter_single_listener() {
use serde_json::json;
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
let port = listener.local_addr().expect("listener addr").port();
let args = json!({
"topic": "ports",
"port": port,
"max_entries": 5
});
let output = match hematite::tools::host_inspect::inspect_host(&args).await {
Ok(output) => output,
Err(err) if err.contains("Failed to run") || err.contains("non-success status") => {
println!("Skipping ports test on this host: {}", err);
return;
}
Err(err) => panic!("inspect host ports failed: {}", err),
};
assert!(output.contains("Host inspection: ports"));
assert!(output.contains(&format!("Filter port: {}", port)));
assert!(output.contains(&format!("127.0.0.1:{}", port)));
}
#[tokio::test]
async fn test_inspect_host_gpo_reports_access_denied_or_objects() {
use serde_json::json;
let args = json!({
"topic": "gpo"
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host gpo");
assert!(output.contains("Host inspection: gpo"));
assert!(
output.contains("Applied Group Policy Objects")
|| output.contains("Error: Access denied")
|| output.contains("No applied Group Policy Objects")
|| output.contains("Windows-only")
);
}
#[tokio::test]
async fn test_inspect_host_certificates_reports_personal_store() {
use serde_json::json;
let args = json!({
"topic": "certificates"
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host certificates");
assert!(output.contains("Host inspection: certificates"));
assert!(
output.contains("Local Machine Certificates")
|| output.contains("No certificates found")
|| output.contains("Cert directory found")
);
}
#[tokio::test]
async fn test_inspect_host_integrity_reports_cbs_health() {
use serde_json::json;
let args = json!({
"topic": "integrity"
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host integrity");
assert!(output.contains("Host inspection: integrity"));
assert!(
output.contains("Windows Component Store Health")
|| output.contains("System integrity check")
|| output.contains("Could not retrieve CBS health")
);
}
#[tokio::test]
async fn test_inspect_host_domain_reports_identity() {
use serde_json::json;
let args = json!({
"topic": "domain"
});
let output = hematite::tools::host_inspect::inspect_host(&args)
.await
.expect("inspect host domain");
assert!(output.contains("Host inspection: domain"));
assert!(
output.contains("Windows Domain / Workgroup Identity")
|| output.contains("Linux Domain Identity")
);
}
#[tokio::test]
async fn test_inspect_host_device_health() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "device_health" }))
.await
.expect("inspect device health fails");
assert!(output.contains("Host inspection: device_health"));
assert!(
output.contains("All PnP devices report as healthy")
|| output.contains("Malfunctioning Devices")
|| output.contains("hardware errors in dmesg")
);
}
#[tokio::test]
async fn test_inspect_host_drivers() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "drivers", "max_entries": 5 }))
.await
.expect("inspect drivers fails");
assert!(output.contains("Host inspection: drivers"));
assert!(
output.contains("Active System Drivers")
|| output.contains("Loaded Kernel Modules")
);
}
#[tokio::test]
async fn test_inspect_host_peripherals() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "peripherals" }))
.await
.expect("inspect peripherals fails");
assert!(output.contains("Host inspection: peripherals"));
assert!(
output.contains("USB Controllers")
|| output.contains("Connected USB Devices")
);
}
#[tokio::test]
async fn test_describe_toolchain_host_inspection_plan_prefers_inspect_host() {
use serde_json::json;
let output = hematite::tools::toolchain::describe_toolchain(&json!({
"topic": "host_inspection_plan",
"question": "How should Hematite inspect my PATH and Downloads folder?"
}))
.await
.expect("describe host inspection plan");
assert!(output.contains("inspect_host"));
assert!(output.contains("optional `shell`"));
assert!(output.contains("PATH"));
}
#[tokio::test]
async fn test_sandbox_python_runs() {
use serde_json::json;
let python_available = std::process::Command::new("python")
.arg("--version")
.output()
.or_else(|_| {
std::process::Command::new("python3")
.arg("--version")
.output()
})
.is_ok();
if !python_available {
println!("Skipping: Python not available");
return;
}
let args = json!({
"language": "python",
"code": "print(2 + 2)"
});
let result = hematite::tools::code_sandbox::execute(&args).await;
assert!(
result.is_ok(),
"Python sandbox should execute: {:?}",
result
);
assert!(result.unwrap().contains("4"), "Should return 4");
}
#[tokio::test]
async fn test_sandbox_javascript_sha256() {
use serde_json::json;
let deno_available = std::process::Command::new("deno")
.arg("--version")
.output()
.is_ok();
let lmstudio_deno = dirs::home_dir()
.map(|h| h.join(".lmstudio/.internal/utils/deno.exe").exists())
.unwrap_or(false);
if !deno_available && !lmstudio_deno {
println!("Skipping: Deno not available");
return;
}
let args = json!({
"language": "javascript",
"code": "const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('Hematite')); console.log([...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,'0')).join(''));"
});
let result = hematite::tools::code_sandbox::execute(&args).await;
assert!(result.is_ok(), "JS sandbox should execute: {:?}", result);
assert!(
result
.unwrap()
.contains("94a194250ccdb8506d67ead15dd3a1db50803855123422f21b378b56f80ba99c"),
"SHA-256 of 'Hematite' should match known hash"
);
}
#[test]
fn test_vein_hot_files_weighted_normalizes_to_one() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().expect("temp db");
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document("src/core.rs", 1_000, "pub fn core() {}")
.unwrap();
vein.index_document("src/util.rs", 2_000, "pub fn util() {}")
.unwrap();
for _ in 0..4 {
vein.bump_heat("src/core.rs");
}
for _ in 0..2 {
vein.bump_heat("src/util.rs");
}
let weighted = vein.hot_files_weighted(10);
assert!(!weighted.is_empty(), "should return weighted hot files");
let core_weight = weighted
.iter()
.find(|(p, _)| p == "src/core.rs")
.map(|(_, w)| *w);
let util_weight = weighted
.iter()
.find(|(p, _)| p == "src/util.rs")
.map(|(_, w)| *w);
assert_eq!(
core_weight,
Some(1.0),
"hottest file should have weight 1.0"
);
let util_w = util_weight.expect("util.rs should appear");
assert!(
(util_w - 0.5).abs() < 0.01,
"util.rs with half the edits should have weight ~0.5, got {}",
util_w
);
}
#[test]
fn test_pagerank_heat_weighted_ranks_active_file_higher() {
use hematite::memory::repo_map::RepoMapGenerator;
use std::fs;
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("core.rs"),
"pub struct Engine {}\npub fn init_engine() -> Engine { Engine {} }\n",
)
.unwrap();
fs::write(
dir.path().join("user.rs"),
"use crate::core::Engine;\nfn use_engine(e: Engine) { let _ = e; }\n",
)
.unwrap();
fs::write(
dir.path().join("admin.rs"),
"use crate::core::Engine;\nfn admin_engine(e: Engine) { let _ = e; }\n",
)
.unwrap();
fs::write(
dir.path().join("leaf.rs"),
"fn unused_leaf_function() {}\nstruct OrphanStruct {}\n",
)
.unwrap();
let hot = vec![
("core.rs".to_string(), 1.0_f64), ("leaf.rs".to_string(), 0.5_f64), ];
let gen = RepoMapGenerator::new(dir.path()).with_hot_files(&hot);
let map = gen.generate().unwrap();
let core_pos = map.find("core.rs:").unwrap_or(usize::MAX);
let leaf_pos = map.find("leaf.rs:").unwrap_or(usize::MAX);
assert!(
core_pos < leaf_pos,
"core.rs (heat=1.0, referenced by 2 files) should rank before leaf.rs (heat=0.5, isolated). Map:\n{}",
map
);
}
#[test]
fn test_edit_file_fuzzy_corrects_indent_on_replace() {
use std::fs;
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
fs::write(
tmp.path(),
"fn outer() {\n fn inner() {\n let x = 1;\n }\n}\n",
)
.unwrap();
let path = tmp.path().to_str().unwrap();
let args = serde_json::json!({
"path": path,
"search": "fn inner() {\n let x = 1;\n}",
"replace": "fn inner() {\n let x = 2;\n}",
});
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(hematite::tools::file_ops::edit_file(&args));
assert!(
result.is_ok(),
"edit should succeed via fuzzy match: {:?}",
result
);
let content = fs::read_to_string(tmp.path()).unwrap();
assert!(
content.contains(" fn inner() {\n let x = 2;\n }"),
"replace should be indent-adjusted to match file indentation:\n{}",
content
);
}
#[test]
fn test_multi_search_replace_fuzzy_corrects_indent() {
use std::fs;
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
fs::write(
tmp.path(),
"impl Foo {\n fn bar(&self) -> u32 {\n 42\n }\n}\n",
)
.unwrap();
let path = tmp.path().to_str().unwrap();
let args = serde_json::json!({
"path": path,
"hunks": [
{
"search": "fn bar(&self) -> u32 {\n 42\n}",
"replace": "fn bar(&self) -> u32 {\n 99\n}"
}
]
});
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(hematite::tools::file_ops::multi_search_replace(&args));
assert!(
result.is_ok(),
"multi_search_replace should succeed via fuzzy: {:?}",
result
);
let content = fs::read_to_string(tmp.path()).unwrap();
assert!(
content.contains(" 99"),
"replacement value should be at correct 8-space indent:\n{}",
content
);
}
#[test]
fn test_edit_file_rstrip_fallback_matches_trailing_spaces() {
use std::fs;
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
fs::write(
tmp.path(),
"fn greet() { \n println!(\"hello\"); \n}\n",
)
.unwrap();
let path = tmp.path().to_str().unwrap();
let args = serde_json::json!({
"path": path,
"search": "fn greet() {\n println!(\"hello\");\n}",
"replace": "fn greet() {\n println!(\"world\");\n}",
});
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(hematite::tools::file_ops::edit_file(&args));
assert!(
result.is_ok(),
"rstrip fallback should match trailing-space file: {:?}",
result
);
let content = fs::read_to_string(tmp.path()).unwrap();
assert!(
content.contains("world"),
"replacement should have applied:\n{}",
content
);
}
#[test]
fn test_edit_file_cross_file_hint_in_error() {
use std::fs;
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let target = dir.path().join("empty.rs");
let sibling = dir.path().join("real.rs");
fs::write(&target, "// nothing here\n").unwrap();
fs::write(&sibling, "fn calculate() {\n 42\n}\n").unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let path = target.to_str().unwrap();
let args = serde_json::json!({
"path": path,
"search": "fn calculate() {\n 42\n}",
"replace": "fn calculate() {\n 99\n}",
});
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(hematite::tools::file_ops::edit_file(&args));
std::env::set_current_dir(original_dir).unwrap();
assert!(result.is_err(), "should fail — search not in target file");
let err = result.unwrap_err();
assert!(
err.contains("real.rs"),
"error should mention the file that actually contains the search string:\n{}",
err
);
}
#[test]
fn test_read_file_returns_full_content_before_conversation_cap() {
use std::fs;
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
let big: String = (0..1000).map(|i| format!("line {:04}\n", i)).collect();
fs::write(tmp.path(), &big).unwrap();
let args = serde_json::json!({ "path": tmp.path().to_str().unwrap() });
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(hematite::tools::file_ops::read_file(&args));
assert!(result.is_ok(), "read_file should succeed on large file");
let content = result.unwrap();
assert!(content.contains("line 0000"), "should have first line");
assert!(content.contains("line 0999"), "should have last line");
}
#[test]
fn test_shell_execute_large_output_accessible() {
let args = serde_json::json!({ "command": "echo hematite-scratch-test" });
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(hematite::tools::shell::execute(&args));
match result {
Ok(out) => assert!(out.contains("hematite-scratch-test") || !out.is_empty()),
Err(e) => println!("shell not available in this env: {}", e),
}
}
#[test]
fn test_detect_memory_type_decision() {
use hematite::memory::vein::detect_memory_type;
assert_eq!(
detect_memory_type("we decided to use SQLite for the vein database"),
"decision"
);
assert_eq!(
detect_memory_type("let's use petgraph for the repo map"),
"decision"
);
assert_eq!(
detect_memory_type("going with AGPL for the license"),
"decision"
);
}
#[test]
fn test_detect_memory_type_problem() {
use hematite::memory::vein::detect_memory_type;
assert_eq!(
detect_memory_type("the issue was that embed model state was not strict"),
"problem"
);
assert_eq!(
detect_memory_type("root cause was a missing CRLF normalization"),
"problem"
);
assert_eq!(
detect_memory_type("fixed by adding the rstrip fallback before full strip"),
"problem"
);
}
#[test]
fn test_detect_memory_type_milestone() {
use hematite::memory::vein::detect_memory_type;
assert_eq!(
detect_memory_type("voice pipeline now working without LM Studio"),
"milestone"
);
assert_eq!(
detect_memory_type("successfully shipped v0.4.5 to crates.io"),
"milestone"
);
}
#[test]
fn test_detect_memory_type_preference() {
use hematite::memory::vein::detect_memory_type;
assert_eq!(
detect_memory_type("i prefer lowercase conventional commits"),
"preference"
);
assert_eq!(
detect_memory_type("i like the diff preview before every edit"),
"preference"
);
}
#[test]
fn test_detect_memory_type_unclassified() {
use hematite::memory::vein::detect_memory_type;
assert_eq!(detect_memory_type("how does the vein indexing work"), "");
assert_eq!(detect_memory_type("read the file and check the output"), "");
}
#[test]
fn test_vein_memory_type_indexed_and_retrieved() {
use hematite::memory::vein::Vein;
let tmp = tempfile::NamedTempFile::new().unwrap();
let mut vein = Vein::new(tmp.path(), "http://127.0.0.1:0".to_string()).expect("vein init");
vein.index_document(
"session/2026-04-12/turn-1",
1_000,
"we decided to use SQLite for local storage because it requires no server",
)
.unwrap();
let results = vein.search_bm25("decided SQLite storage", 10).unwrap();
assert!(!results.is_empty(), "should find the session chunk");
let hit = results.iter().find(|r| r.path.contains("turn-1"));
assert!(hit.is_some(), "should find the specific turn");
assert_eq!(
hit.unwrap().memory_type,
"decision",
"session chunk with 'decided' should be tagged as decision"
);
}
#[test]
fn test_shell_streaming_emits_shell_line_events() {
use hematite::agent::inference::InferenceEvent;
use tokio::sync::mpsc;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let (tx, mut rx) = mpsc::channel::<InferenceEvent>(128);
let args = serde_json::json!({ "command": "echo streaming-test" });
let result = hematite::tools::shell::execute_streaming(&args, tx).await;
let mut shell_lines: Vec<String> = Vec::new();
while let Ok(event) = rx.try_recv() {
if let InferenceEvent::ShellLine(line) = event {
shell_lines.push(line);
}
}
match result {
Ok(output) => {
assert!(
!shell_lines.is_empty(),
"execute_streaming should emit ShellLine events; got none"
);
assert!(
output.contains("streaming-test"),
"buffered output should contain echo content; got: {output}"
);
let streamed = shell_lines.join("\n");
assert!(
streamed.contains("streaming-test"),
"streamed lines should contain echo content; got: {streamed}"
);
}
Err(e) => println!("shell not available in this env: {e}"),
}
});
}
#[test]
fn test_shell_streaming_buffered_output_matches_blocking() {
use hematite::agent::inference::InferenceEvent;
use tokio::sync::mpsc;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "command": "echo consistent-output" });
let blocking = hematite::tools::shell::execute(&args).await;
let (tx, mut rx) = mpsc::channel::<InferenceEvent>(128);
let streaming = hematite::tools::shell::execute_streaming(&args, tx).await;
while rx.try_recv().is_ok() {}
match (blocking, streaming) {
(Ok(b), Ok(s)) => {
assert!(
b.contains("consistent-output") && s.contains("consistent-output"),
"both paths should contain echo output; blocking={b:?} streaming={s:?}"
);
}
(Err(e), _) | (_, Err(e)) => println!("shell not available in this env: {e}"),
}
});
}
#[test]
fn test_checkpoint_load_returns_none_when_no_session_file() {
let result = std::panic::catch_unwind(hematite::agent::conversation::load_checkpoint);
assert!(result.is_ok(), "load_checkpoint should never panic");
}
#[test]
fn test_checkpoint_roundtrip_via_session_json() {
use std::io::Write;
let temp_workspace = tempfile::tempdir().expect("failed to create temp workspace");
let session_path = temp_workspace.path().join("session.json");
std::env::set_var("HEMATITE_SESSION_PATH", &session_path);
let fake = serde_json::json!({
"running_summary": null,
"session_memory": {
"current_task": "implement streaming shell output",
"working_set": ["src/tools/shell.rs", "src/agent/conversation.rs"],
"learnings": [],
"last_verification": { "successful": true, "summary": "cargo test ok" }
},
"last_goal": "add streaming shell and diagnostics",
"turn_count": 7
});
{
let mut f = std::fs::File::create(&session_path).expect("Failed to create fake session.json");
write!(f, "{}", fake).expect("Failed to write fake session.json");
}
let cp = hematite::agent::conversation::load_checkpoint();
std::env::remove_var("HEMATITE_SESSION_PATH");
let cp = cp.expect("load_checkpoint should return Some for a valid prior session");
assert_eq!(cp.turn_count, 7);
assert_eq!(cp.last_goal, "add streaming shell and diagnostics");
assert_eq!(cp.last_verify_ok, Some(true));
assert!(
cp.working_files.contains(&"src/tools/shell.rs".to_string())
|| cp
.working_files
.contains(&"src/agent/conversation.rs".to_string()),
"working_files should include files from working_set"
);
}
#[test]
fn test_extract_memory_working_set_spans_all_turns() {
use hematite::agent::compaction::extract_memory;
use hematite::agent::inference::ChatMessage;
fn tool_call_msg(path: &str) -> ChatMessage {
let mut m = ChatMessage::assistant_text("");
m.tool_calls = vec![hematite::agent::inference::ToolCallResponse {
id: "x".into(),
call_type: "function".into(),
function: hematite::agent::inference::ToolCallFn {
name: "edit_file".into(),
arguments: format!(r#"{{"path": "{path}", "search": "a", "replace": "b"}}"#),
},
}];
m
}
let messages = vec![
ChatMessage::system("sys"),
ChatMessage::user("first turn"),
tool_call_msg("src/early_file.rs"),
ChatMessage::user("second turn"),
tool_call_msg("src/later_file.rs"),
ChatMessage::user("third turn — most recent"),
tool_call_msg("src/newest_file.rs"),
];
let mem = extract_memory(&messages);
assert!(
mem.working_set.contains("src/early_file.rs"),
"early file should survive across turns; got {:?}",
mem.working_set
);
assert!(mem.working_set.contains("src/later_file.rs"));
assert!(mem.working_set.contains("src/newest_file.rs"));
assert!(mem.current_task.contains("most recent"));
}
#[test]
fn test_build_summary_captures_verify_build_outcome() {
use hematite::agent::compaction::compact_history;
use hematite::agent::compaction::CompactionConfig;
use hematite::agent::inference::ChatMessage;
let mut messages = vec![ChatMessage::system("sys")];
for i in 0..30 {
messages.push(ChatMessage::user(&format!("do task {i}")));
let mut assistant = ChatMessage::assistant_text("");
assistant.tool_calls = vec![hematite::agent::inference::ToolCallResponse {
id: format!("c{i}"),
call_type: "function".into(),
function: hematite::agent::inference::ToolCallFn {
name: "verify_build".into(),
arguments: "{}".into(),
},
}];
messages.push(assistant);
let mut tool_result = ChatMessage::user("BUILD OK — cargo build passed");
tool_result.role = "tool".into();
messages.push(tool_result);
}
let config = CompactionConfig {
preserve_recent_messages: 6,
max_estimated_tokens: 100, };
let result = compact_history(&messages, None, config, Some(1));
let summary_msg = result
.messages
.iter()
.find(|m| m.role == "system" && m.content.as_str().contains("CONTEXT SUMMARY"));
assert!(
summary_msg.is_some(),
"compaction should produce a summary system message"
);
let summary_text = summary_msg.unwrap().content.as_str();
assert!(
summary_text.contains("BUILD OK") || summary_text.contains("verify_build"),
"summary should capture verify_build outcome; got:\n{summary_text}"
);
}
#[test]
fn test_verify_build_streaming_no_project_emits_no_shell_lines() {
use hematite::agent::inference::InferenceEvent;
use tokio::sync::mpsc;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let tmp = std::env::temp_dir().join("hematite_vb_streaming_test");
std::fs::create_dir_all(&tmp).unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(&tmp).unwrap();
let (tx, mut rx) = mpsc::channel::<InferenceEvent>(32);
let args = serde_json::json!({ "action": "build" });
let result = hematite::tools::verify_build::execute_streaming(&args, tx).await;
std::env::set_current_dir(&original).unwrap();
let mut shell_line_count = 0usize;
while let Ok(ev) = rx.try_recv() {
if matches!(ev, InferenceEvent::ShellLine(_)) {
shell_line_count += 1;
}
}
assert_eq!(
shell_line_count, 0,
"no ShellLine events expected when autodetect fails"
);
assert!(
result.is_err(),
"execute_streaming should return Err when no project is detected"
);
let msg = result.unwrap_err();
assert!(
msg.contains("No recognized project root"),
"error should explain the missing project root; got: {msg}"
);
});
}
#[test]
fn test_verify_build_streaming_output_shape_matches_blocking() {
use tokio::sync::mpsc;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let tmp = std::env::temp_dir().join("hematite_vb_shape_test");
std::fs::create_dir_all(&tmp).unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(&tmp).unwrap();
let args = serde_json::json!({ "action": "build" });
let blocking = hematite::tools::verify_build::execute(&args).await;
let (tx, mut rx) =
mpsc::channel::<hematite::agent::inference::InferenceEvent>(32);
let streaming = hematite::tools::verify_build::execute_streaming(&args, tx).await;
while rx.try_recv().is_ok() {}
std::env::set_current_dir(&original).unwrap();
assert_eq!(
blocking.is_ok(),
streaming.is_ok(),
"blocking and streaming must agree on Ok/Err; blocking={blocking:?} streaming={streaming:?}"
);
});
}
#[test]
fn test_tail_file_returns_last_n_lines() {
use hematite::tools::file_ops::tail_file;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let tmp_path = std::env::temp_dir().join("hematite_tail_test.txt");
let content = (1..=10u32)
.map(|i| format!("line {i}"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&tmp_path, &content).unwrap();
let args = serde_json::json!({
"path": tmp_path.to_string_lossy(),
"lines": 3
});
let result = tail_file(&args).await.unwrap();
assert!(
result.contains("line 8"),
"tail should include line 8; got:\n{result}"
);
assert!(
result.contains("line 9"),
"tail should include line 9; got:\n{result}"
);
assert!(
result.contains("line 10"),
"tail should include line 10; got:\n{result}"
);
assert!(
!result.contains("line 7"),
"tail should NOT include line 7 when lines=3; got:\n{result}"
);
assert!(
result.contains("10"),
"header should mention total line count; got:\n{result}"
);
let _ = std::fs::remove_file(&tmp_path);
});
}
#[test]
fn test_tail_file_grep_filter_matches_only_relevant_lines() {
use hematite::tools::file_ops::tail_file;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let tmp_path = std::env::temp_dir().join("hematite_tail_grep_test.txt");
let lines = vec![
"info: starting build",
"error: E0425 cannot find value",
"info: compiling foo.rs",
"error: E0308 type mismatch",
"info: build finished",
];
std::fs::write(&tmp_path, lines.join("\n")).unwrap();
let args = serde_json::json!({
"path": tmp_path.to_string_lossy(),
"grep": "error"
});
let result = tail_file(&args).await.unwrap();
assert!(
result.contains("E0425"),
"grep=error should include the E0425 error line; got:\n{result}"
);
assert!(
result.contains("E0308"),
"grep=error should include the E0308 error line; got:\n{result}"
);
assert!(
!result.contains("compiling"),
"grep=error should exclude non-error lines; got:\n{result}"
);
assert!(
!result.contains("build finished"),
"grep=error should exclude info lines; got:\n{result}"
);
let _ = std::fs::remove_file(&tmp_path);
});
}
#[test]
fn test_tail_file_missing_file_returns_err() {
use hematite::tools::file_ops::tail_file;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "path": "/nonexistent/path/to/file.log" });
let result = tail_file(&args).await;
assert!(
result.is_err(),
"tail_file on a missing file must return Err"
);
});
}
#[test]
fn test_tail_file_lines_default_is_fifty() {
use hematite::tools::file_ops::tail_file;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let tmp_path = std::env::temp_dir().join("hematite_tail_default_test.txt");
let content = (1..=60u32)
.map(|i| format!("row {i}"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&tmp_path, &content).unwrap();
let args = serde_json::json!({ "path": tmp_path.to_string_lossy() });
let result = tail_file(&args).await.unwrap();
assert!(
result.contains("row 60"),
"default tail must include last line"
);
assert!(
result.contains("row 11"),
"default tail must include row 11 (60-50=10, so 11 is the first)"
);
assert!(
!result.contains("row 10"),
"default tail must NOT include row 10 (outside 50-line window)"
);
let _ = std::fs::remove_file(&tmp_path);
});
}
#[test]
fn test_inspect_host_log_check_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "log_check", "max_entries": 5 });
let result = inspect_host(&args).await;
let output = result.expect("log_check must return Ok, not Err");
assert!(
output.contains("log_check"),
"log_check output must contain the topic name as a header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_startup_items_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "startup_items", "max_entries": 10 });
let result = inspect_host(&args).await;
let output = result.expect("startup_items must return Ok, not Err");
assert!(
output.contains("startup_items"),
"startup_items output must contain the topic name as a header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_unknown_topic_includes_new_topics_in_error() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "nonexistent_topic_xyz" });
let result = inspect_host(&args).await;
let err = result.expect_err("unknown topic must return Err");
assert!(
err.contains("log_check"),
"unknown-topic error must mention log_check; got:\n{err}"
);
assert!(
err.contains("startup_items"),
"unknown-topic error must mention startup_items; got:\n{err}"
);
assert!(
err.contains("storage"),
"unknown-topic error must mention storage; got:\n{err}"
);
assert!(
err.contains("hardware"),
"unknown-topic error must mention hardware; got:\n{err}"
);
assert!(
err.contains("health_report"),
"unknown-topic error must mention health_report; got:\n{err}"
);
});
}
#[test]
fn test_inspect_host_health_report_returns_verdict() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "health_report" });
let output = inspect_host(&args)
.await
.expect("health_report must return Ok");
let has_verdict = output.contains("ALL GOOD")
|| output.contains("WORTH A LOOK")
|| output.contains("ACTION REQUIRED");
assert!(
has_verdict,
"health_report must include a verdict; got:\n{output}"
);
assert!(
output.contains("System Health Report"),
"health_report must include the header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_health_report_sections_are_non_empty() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "health_report" });
let output = inspect_host(&args)
.await
.expect("health_report must return Ok");
let has_section = output.contains("Looking good:")
|| output.contains("Worth watching:")
|| output.contains("Needs fixing:");
assert!(
has_section,
"health_report must include at least one categorized section; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_storage_returns_drive_info() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "storage" });
let output = inspect_host(&args).await.expect("storage must return Ok");
assert!(
output.contains("storage"),
"storage output must contain topic header; got:\n{output}"
);
assert!(
output.contains("Drives:") || output.contains("drive") || output.contains("GB"),
"storage output must describe drive capacity; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_storage_includes_cache_section() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "storage" });
let output = inspect_host(&args).await.expect("storage must return Ok");
assert!(
output.contains("cache") || output.contains("Cache"),
"storage output must include a cache directory section; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_hardware_returns_cpu_info() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "hardware" });
let output = inspect_host(&args).await.expect("hardware must return Ok");
assert!(
output.contains("hardware"),
"hardware output must contain topic header; got:\n{output}"
);
assert!(
output.contains("CPU") || output.contains("processor") || output.contains("core"),
"hardware output must include CPU information; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_hardware_returns_gpu_or_ram() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "hardware" });
let output = inspect_host(&args).await.expect("hardware must return Ok");
let has_gpu_or_ram =
output.contains("GPU") || output.contains("RAM") || output.contains("GB");
assert!(
has_gpu_or_ram,
"hardware output must include GPU or RAM details; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_updates_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "updates" });
let output = inspect_host(&args).await.expect("updates must return Ok");
assert!(
output.contains("updates"),
"updates output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_updates_contains_update_info() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "updates" });
let output = inspect_host(&args).await.expect("updates must return Ok");
let has_info = output.contains("Last update")
|| output.contains("Pending")
|| output.contains("service")
|| output.contains("up to date")
|| output.contains("unable")
|| output.contains("package");
assert!(
has_info,
"updates output must contain meaningful update info; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_security_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "security" });
let output = inspect_host(&args).await.expect("security must return Ok");
assert!(
output.contains("security"),
"security output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_security_reports_protection_status() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "security" });
let output = inspect_host(&args).await.expect("security must return Ok");
let has_info = output.contains("Defender")
|| output.contains("Firewall")
|| output.contains("activation")
|| output.contains("UAC")
|| output.contains("protection")
|| output.contains("UFW")
|| output.contains("unable");
assert!(
has_info,
"security output must report protection status; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_pending_reboot_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "pending_reboot" });
let output = inspect_host(&args)
.await
.expect("pending_reboot must return Ok");
assert!(
output.contains("pending_reboot"),
"pending_reboot output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_pending_reboot_gives_verdict() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "pending_reboot" });
let output = inspect_host(&args)
.await
.expect("pending_reboot must return Ok");
let has_verdict = output.contains("No restart")
|| output.contains("restart is pending")
|| output.contains("Could not")
|| output.contains("reboot-required");
assert!(
has_verdict,
"pending_reboot must give a clear verdict; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_disk_health_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "disk_health" });
let output = inspect_host(&args)
.await
.expect("disk_health must return Ok");
assert!(
output.contains("disk_health"),
"disk_health output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_disk_health_reports_drive_info() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "disk_health" });
let output = inspect_host(&args)
.await
.expect("disk_health must return Ok");
let has_info = output.contains("Health")
|| output.contains("Drive")
|| output.contains("GB")
|| output.contains("No physical")
|| output.contains("Unable")
|| output.contains("NAME")
|| output.contains("smartmontools");
assert!(
has_info,
"disk_health must report drive info or explain unavailability; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_battery_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "battery" });
let output = inspect_host(&args).await.expect("battery must return Ok");
assert!(
output.contains("battery"),
"battery output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_battery_reports_status_or_no_battery() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "battery" });
let output = inspect_host(&args).await.expect("battery must return Ok");
let has_info = output.contains("Battery:")
|| output.contains("No battery")
|| output.contains("desktop")
|| output.contains("Charge")
|| output.contains("Unable")
|| output.contains("AC-only");
assert!(
has_info,
"battery must report charge status or explain no battery; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_recent_crashes_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "recent_crashes" });
let output = inspect_host(&args)
.await
.expect("recent_crashes must return Ok");
assert!(
output.contains("recent_crashes"),
"recent_crashes output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_recent_crashes_reports_crash_info_or_none() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "recent_crashes" });
let output = inspect_host(&args)
.await
.expect("recent_crashes must return Ok");
let has_info = output.contains("None in recent")
|| output.contains("crashes")
|| output.contains("BSOD")
|| output.contains("shutdown")
|| output.contains("unable")
|| output.contains("No kernel");
assert!(
has_info,
"recent_crashes must report crash history or explain unavailability; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_scheduled_tasks_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "scheduled_tasks" });
let output = inspect_host(&args)
.await
.expect("scheduled_tasks must return Ok");
assert!(
output.contains("scheduled_tasks"),
"scheduled_tasks output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_scheduled_tasks_reports_tasks_or_explains() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "scheduled_tasks" });
let output = inspect_host(&args)
.await
.expect("scheduled_tasks must return Ok");
let has_info = output.contains("State:")
|| output.contains("Last run:")
|| output.contains("No active")
|| output.contains("Unable")
|| output.contains("timers")
|| output.contains("crontab");
assert!(
has_info,
"scheduled_tasks must list tasks or explain availability; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_dev_conflicts_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "dev_conflicts" });
let output = inspect_host(&args)
.await
.expect("dev_conflicts must return Ok");
assert!(
output.contains("dev_conflicts"),
"dev_conflicts output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_dev_conflicts_checks_major_runtimes() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "dev_conflicts" });
let output = inspect_host(&args)
.await
.expect("dev_conflicts must return Ok");
let checks_node = output.contains("Node.js");
let checks_python = output.contains("Python");
let checks_git = output.contains("Git");
assert!(
checks_node && checks_python && checks_git,
"dev_conflicts must check Node.js, Python, and Git; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_dev_conflicts_gives_summary_verdict() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "dev_conflicts" });
let output = inspect_host(&args)
.await
.expect("dev_conflicts must return Ok");
let has_verdict = output.contains("No conflicts")
|| output.contains("CONFLICTS")
|| output.contains("NOTES")
|| output.contains("[!]")
|| output.contains("[-]");
assert!(
has_verdict,
"dev_conflicts must end with a summary verdict; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_unknown_topic_includes_all_new_topics_in_error() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "nonexistent_topic_xyz" });
let err = inspect_host(&args)
.await
.expect_err("unknown topic must return Err");
let new_topics = [
"updates", "security", "pending_reboot", "disk_health", "battery",
"recent_crashes", "scheduled_tasks", "dev_conflicts",
"docker", "wsl", "ssh", "env", "hosts_file", "installed_software", "git_config",
];
for topic in new_topics {
assert!(
err.contains(topic),
"error message must list '{topic}' as a valid topic; got:\n{err}"
);
}
});
}
#[test]
fn test_inspect_host_env_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "env" });
let output = inspect_host(&args).await.expect("env must return Ok");
assert!(
output.contains("Host inspection: env"),
"env output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_env_shows_total_and_path_note() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "env" });
let output = inspect_host(&args).await.expect("env must return Ok");
assert!(
output.contains("Total environment variables:"),
"env output must show total count; got:\n{output}"
);
assert!(
output.contains("PATH:"),
"env output must note PATH entry count; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_hosts_file_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "hosts_file" });
let output = inspect_host(&args).await.expect("hosts_file must return Ok");
assert!(
output.contains("Host inspection: hosts_file"),
"hosts_file output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_hosts_file_shows_path_and_summary() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "hosts_file" });
let output = inspect_host(&args).await.expect("hosts_file must return Ok");
let has_path = output.contains("Path:") && (output.contains("hosts") || output.contains("etc"));
let has_summary = output.contains("Active entries:") || output.contains("Could not read");
assert!(has_path, "hosts_file must show file path; got:\n{output}");
assert!(has_summary, "hosts_file must show entry summary or error; got:\n{output}");
});
}
#[test]
fn test_inspect_host_docker_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "docker" });
let output = inspect_host(&args).await.expect("docker must return Ok");
assert!(
output.contains("Host inspection: docker"),
"docker output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_docker_reports_status_or_not_found() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "docker" });
let output = inspect_host(&args).await.expect("docker must return Ok");
let has_result = output.contains("Docker Engine:")
|| output.contains("not found")
|| output.contains("daemon is NOT running")
|| output.contains("error");
assert!(
has_result,
"docker must report engine version, not-found, or daemon-down; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_wsl_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "wsl" });
let output = inspect_host(&args).await.expect("wsl must return Ok");
assert!(
output.contains("Host inspection: wsl"),
"wsl output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_wsl_reports_distros_or_status() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "wsl" });
let output = inspect_host(&args).await.expect("wsl must return Ok");
let has_result = output.contains("WSL Distributions")
|| output.contains("not installed")
|| output.contains("no distributions")
|| output.contains("Windows-only feature")
|| output.contains("wsl --install")
|| output.contains("error");
assert!(
has_result,
"wsl must report distros, install hint, or platform note; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_ssh_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "ssh" });
let output = inspect_host(&args).await.expect("ssh must return Ok");
assert!(
output.contains("Host inspection: ssh"),
"ssh output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_ssh_reports_client_and_dotsssh() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "ssh" });
let output = inspect_host(&args).await.expect("ssh must return Ok");
let has_client = output.contains("SSH client:")
|| output.contains("not found on PATH");
let has_ssh_dir = output.contains("~/.ssh:")
|| output.contains("not found");
assert!(has_client, "ssh must report client version or not-found; got:\n{output}");
assert!(has_ssh_dir, "ssh must report ~/.ssh state; got:\n{output}");
});
}
#[test]
fn test_inspect_host_installed_software_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "installed_software" });
let output = inspect_host(&args).await.expect("installed_software must return Ok");
assert!(
output.contains("Host inspection: installed_software"),
"installed_software output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_installed_software_lists_packages_or_explains() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "installed_software" });
let output = inspect_host(&args).await.expect("installed_software must return Ok");
let has_result = output.contains("packages")
|| output.contains("Installed software")
|| output.contains("Homebrew")
|| output.contains("dpkg")
|| output.contains("rpm")
|| output.contains("pacman")
|| output.contains("failed")
|| output.contains("not found");
assert!(
has_result,
"installed_software must list packages or explain why not; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_git_config_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "git_config" });
let output = inspect_host(&args).await.expect("git_config must return Ok");
assert!(
output.contains("Host inspection: git_config"),
"git_config output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_git_config_reports_version_and_config() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "git_config" });
let output = inspect_host(&args).await.expect("git_config must return Ok");
let has_git = output.contains("Git:") || output.contains("not found");
assert!(has_git, "git_config must report git version or not-found; got:\n{output}");
if output.contains("Git: git version") {
let has_config = output.to_lowercase().contains("global git config");
assert!(has_config, "git_config must show global config section; got:\n{output}");
}
});
}
#[test]
fn test_routing_detects_docker_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("are any docker containers running?"),
Some("docker")
);
assert_eq!(
preferred_host_inspection_topic("show me my docker images"),
Some("docker")
);
}
#[test]
fn test_routing_detects_wsl_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("what wsl distros do I have?"),
Some("wsl")
);
assert_eq!(
preferred_host_inspection_topic("show me windows subsystem for linux distros"),
Some("wsl")
);
}
#[test]
fn test_routing_detects_ssh_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("show me my ssh config"),
Some("ssh")
);
assert_eq!(
preferred_host_inspection_topic("how many known_hosts do I have?"),
Some("ssh")
);
}
#[test]
fn test_routing_detects_git_config_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("show me my git config"),
Some("git_config")
);
assert_eq!(
preferred_host_inspection_topic("what is my git global user.name?"),
Some("git_config")
);
}
#[test]
fn test_routing_detects_installed_software_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("what software is installed on this machine?"),
Some("installed_software")
);
assert_eq!(
preferred_host_inspection_topic("show me installed programs"),
Some("installed_software")
);
}
#[test]
fn test_routing_detects_env_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("show me my environment variables"),
Some("env")
);
assert_eq!(
preferred_host_inspection_topic("list env vars"),
Some("env")
);
}
#[test]
fn test_routing_detects_hosts_file_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("show me the hosts file"),
Some("hosts_file")
);
assert_eq!(
preferred_host_inspection_topic("what is in /etc/hosts?"),
Some("hosts_file")
);
}
#[test]
fn test_all_host_topics_detects_docker_and_ssh_together() {
use hematite::agent::routing::all_host_inspection_topics;
let topics = all_host_inspection_topics("show me docker containers and my ssh config");
assert!(topics.contains(&"docker"), "should detect docker; got: {topics:?}");
assert!(topics.contains(&"ssh"), "should detect ssh; got: {topics:?}");
assert!(topics.len() >= 2, "should detect 2+ topics; got: {topics:?}");
}
#[test]
fn test_inspect_host_databases_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "databases" });
let output = inspect_host(&args).await.expect("databases must return Ok");
assert!(
output.contains("Host inspection: databases"),
"databases output must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_databases_reports_found_or_not_found() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "databases" });
let output = inspect_host(&args).await.expect("databases must return Ok");
let has_result = output.contains("[FOUND]")
|| output.contains("No local database engines detected");
assert!(
has_result,
"databases must report found engines or explicit not-found; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_databases_mentions_docker_note() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "databases" });
let output = inspect_host(&args).await.expect("databases must return Ok");
assert!(
output.contains("Docker"),
"databases must note that Docker containers are covered by topic=docker; got:\n{output}"
);
});
}
#[test]
fn test_routing_detects_databases_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("is postgres running on this machine?"),
Some("databases")
);
assert_eq!(
preferred_host_inspection_topic("what databases are installed locally?"),
Some("databases")
);
assert_eq!(
preferred_host_inspection_topic("is redis up?"),
Some("databases")
);
}
#[test]
fn test_fix_plan_driver_install_returns_grounded_steps() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "how do I install a GPU driver?" });
let output = inspect_host(&args).await.expect("fix_plan driver_install must return Ok");
assert!(
output.contains("fix_plan") && output.contains("driver"),
"driver_install fix_plan must contain driver guidance; got:\n{output}"
);
assert!(
output.contains("Device Manager") || output.contains("manufacturer"),
"driver_install fix_plan must mention Device Manager or manufacturer download; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_ssh_key_reports_key_state() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "generate ssh key pair" });
let output = inspect_host(&args).await.expect("fix_plan ssh_key must return Ok");
assert!(
output.contains("id_ed25519") || output.contains("ssh-keygen"),
"ssh_key fix_plan must mention id_ed25519 or ssh-keygen; got:\n{output}"
);
let has_key_state = output.contains("id_ed25519 key found:")
|| output.contains("id_rsa key found:");
assert!(
has_key_state,
"ssh_key fix_plan must report whether keys exist; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_wsl_setup_returns_install_steps() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "how do I install WSL2?" });
let output = inspect_host(&args).await.expect("fix_plan wsl_setup must return Ok");
assert!(
output.contains("wsl") || output.contains("WSL"),
"wsl_setup fix_plan must contain WSL guidance; got:\n{output}"
);
assert!(
output.contains("wsl --install") || output.contains("WSL already installed"),
"wsl_setup fix_plan must mention wsl --install or note already installed; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_firewall_rule_returns_powershell_commands() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "create a firewall rule to open port 8080" });
let output = inspect_host(&args).await.expect("fix_plan firewall_rule must return Ok");
assert!(
output.contains("New-NetFirewallRule"),
"firewall_rule fix_plan must include New-NetFirewallRule command; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_disk_cleanup_returns_cleanup_steps() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "free up disk space my drive is almost full" });
let output = inspect_host(&args).await.expect("fix_plan disk_cleanup must return Ok");
assert!(
output.contains("cleanmgr") || output.contains("Disk Cleanup") || output.contains("SoftwareDistribution"),
"disk_cleanup fix_plan must mention cleanup tools; got:\n{output}"
);
assert!(
output.contains("cargo clean") || output.contains("npm cache"),
"disk_cleanup fix_plan must mention developer cache cleanup; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_scheduled_task_returns_register_command() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "create a scheduled task to run my script every day" });
let output = inspect_host(&args).await.expect("fix_plan scheduled_task must return Ok");
assert!(
output.contains("Register-ScheduledTask"),
"scheduled_task fix_plan must include Register-ScheduledTask command; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_registry_edit_warns_and_shows_backup() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "add a registry key in HKLM" });
let output = inspect_host(&args).await.expect("fix_plan registry_edit must return Ok");
assert!(
output.contains("reg export") || output.contains("backup"),
"registry_edit fix_plan must mention backup/export step; got:\n{output}"
);
assert!(
output.contains("Set-ItemProperty") || output.contains("New-Item"),
"registry_edit fix_plan must include PowerShell registry commands; got:\n{output}"
);
});
}
#[test]
fn test_fix_plan_generic_lists_all_lanes() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "fix_plan", "issue": "completely unrelated thing not matching any lane" });
let output = inspect_host(&args).await.expect("fix_plan generic must return Ok");
assert!(
output.contains("Firewall rule") || output.contains("SSH key") || output.contains("Disk cleanup"),
"generic fix_plan must list available lanes; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_user_accounts_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "user_accounts" });
let output = inspect_host(&args).await.expect("user_accounts must return Ok");
assert!(
output.contains("Host inspection: user_accounts"),
"user_accounts must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_user_accounts_reports_local_users_or_sessions() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "user_accounts" });
let output = inspect_host(&args).await.expect("user_accounts must return Ok");
let has_section = output.contains("Local User Accounts")
|| output.contains("Active Sessions")
|| output.contains("Active Logon Sessions");
assert!(
has_section,
"user_accounts must contain a user or session section; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_user_accounts_reports_elevation() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "user_accounts" });
let output = inspect_host(&args).await.expect("user_accounts must return Ok");
assert!(
output.contains("Administrator") || output.contains("Elevation") || output.contains("elevated"),
"user_accounts must report elevation state or admin group; got:\n{output}"
);
});
}
#[test]
fn test_routing_detects_user_accounts_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("who is logged in right now?"),
Some("user_accounts")
);
assert_eq!(
preferred_host_inspection_topic("show me local user accounts"),
Some("user_accounts")
);
assert_eq!(
preferred_host_inspection_topic("who has admin rights on this machine?"),
Some("user_accounts")
);
}
#[test]
fn test_inspect_host_audit_policy_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "audit_policy" });
let output = inspect_host(&args).await.expect("audit_policy must return Ok");
assert!(
output.contains("Host inspection: audit_policy"),
"audit_policy must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_audit_policy_reports_policy_or_elevation_required() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "audit_policy" });
let output = inspect_host(&args).await.expect("audit_policy must return Ok");
let has_result = output.contains("Audit Policy")
|| output.contains("ENABLED")
|| output.contains("No Auditing")
|| output.contains("requires Administrator")
|| output.contains("auditd")
|| output.contains("WARNING");
assert!(
has_result,
"audit_policy must report policy state or note elevation required; got:\n{output}"
);
});
}
#[test]
fn test_routing_detects_audit_policy_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("what is the audit policy on this machine?"),
Some("audit_policy")
);
assert_eq!(
preferred_host_inspection_topic("is security auditing enabled?"),
Some("audit_policy")
);
}
#[test]
fn test_inspect_host_shares_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "shares" });
let output = inspect_host(&args).await.expect("shares must return Ok");
assert!(
output.contains("Host inspection: shares"),
"shares must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_shares_reports_smb_section() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "shares" });
let output = inspect_host(&args).await.expect("shares must return Ok");
let has_section = output.contains("SMB") || output.contains("Samba") || output.contains("NFS");
assert!(
has_section,
"shares must contain SMB, Samba, or NFS section; got:\n{output}"
);
});
}
#[test]
fn test_routing_detects_shares_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("what SMB shares does this machine have?"),
Some("shares")
);
assert_eq!(
preferred_host_inspection_topic("show me mapped network drives"),
Some("shares")
);
assert_eq!(
preferred_host_inspection_topic("is SMB1 enabled on this machine?"),
Some("shares")
);
}
#[test]
fn test_inspect_host_dns_servers_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "dns_servers" });
let output = inspect_host(&args).await.expect("dns_servers must return Ok");
assert!(
output.contains("Host inspection: dns_servers"),
"dns_servers must contain header; got:\n{output}"
);
});
}
#[test]
fn test_inspect_host_dns_servers_reports_resolvers_or_resolv_conf() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "dns_servers" });
let output = inspect_host(&args).await.expect("dns_servers must return Ok");
let has_section = output.contains("DNS Resolver")
|| output.contains("resolv.conf")
|| output.contains("Configured DNS")
|| output.contains("systemd-resolved");
assert!(
has_section,
"dns_servers must report DNS resolver config; got:\n{output}"
);
});
}
#[test]
fn test_routing_detects_dns_servers_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(
preferred_host_inspection_topic("what DNS servers am I using?"),
Some("dns_servers")
);
assert_eq!(
preferred_host_inspection_topic("show me the configured DNS resolver"),
Some("dns_servers")
);
assert_eq!(
preferred_host_inspection_topic("is DNS over HTTPS configured?"),
Some("dns_servers")
);
}
#[test]
fn test_inspect_host_bitlocker_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "bitlocker" });
let output = inspect_host(&args).await.expect("bitlocker must return Ok");
assert!(output.contains("Host inspection: bitlocker"));
});
}
#[test]
fn test_routing_detects_bitlocker_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("is my drive encrypted?"), Some("bitlocker"));
assert_eq!(preferred_host_inspection_topic("bitlocker status"), Some("bitlocker"));
}
#[test]
fn test_inspect_host_rdp_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "rdp" });
let output = inspect_host(&args).await.expect("rdp must return Ok");
assert!(output.contains("Host inspection: rdp"));
});
}
#[test]
fn test_routing_detects_rdp_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("is remote desktop enabled?"), Some("rdp"));
assert_eq!(preferred_host_inspection_topic("show RDP settings"), Some("rdp"));
}
#[test]
fn test_inspect_host_shadow_copies_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "shadow_copies" });
let output = inspect_host(&args).await.expect("shadow_copies must return Ok");
assert!(output.contains("Host inspection: shadow_copies"));
});
}
#[test]
fn test_routing_detects_shadow_copies_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("show me shadow copies"), Some("shadow_copies"));
assert_eq!(preferred_host_inspection_topic("VSS snapshots"), Some("shadow_copies"));
}
#[test]
fn test_inspect_host_pagefile_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "pagefile" });
let output = inspect_host(&args).await.expect("pagefile must return Ok");
assert!(output.contains("Host inspection: pagefile"));
});
}
#[test]
fn test_routing_detects_pagefile_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("how big is my pagefile?"), Some("pagefile"));
assert_eq!(preferred_host_inspection_topic("virtual memory usage"), Some("pagefile"));
}
#[test]
fn test_inspect_host_windows_features_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "windows_features" });
let output = inspect_host(&args).await.expect("windows_features must return Ok");
assert!(output.contains("Host inspection: windows_features"));
});
}
#[test]
fn test_routing_detects_windows_features_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("what windows features are on?"), Some("windows_features"));
assert_eq!(preferred_host_inspection_topic("is IIS installed?"), Some("windows_features"));
}
#[test]
fn test_inspect_host_printers_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "printers" });
let output = inspect_host(&args).await.expect("printers must return Ok");
assert!(output.contains("Host inspection: printers"));
});
}
#[test]
fn test_routing_detects_printers_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("list my printers"), Some("printers"));
assert_eq!(preferred_host_inspection_topic("is anything in the print queue?"), Some("printers"));
}
#[test]
fn test_inspect_host_winrm_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "winrm" });
let output = inspect_host(&args).await.expect("winrm must return Ok");
assert!(output.contains("Host inspection: winrm"));
});
}
#[test]
fn test_routing_detects_winrm_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("is WinRM enabled?"), Some("winrm"));
assert_eq!(preferred_host_inspection_topic("check PS Remoting status"), Some("winrm"));
}
#[test]
fn test_inspect_host_network_stats_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "network_stats" });
let output = inspect_host(&args).await.expect("network_stats must return Ok");
assert!(output.contains("Host inspection: network_stats"));
});
}
#[test]
fn test_routing_detects_network_stats_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("adapter throughput stats"), Some("network_stats"));
assert_eq!(preferred_host_inspection_topic("any dropped packets on my NIC?"), Some("network_stats"));
}
#[test]
fn test_inspect_host_udp_ports_returns_header() {
use hematite::tools::host_inspect::inspect_host;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let args = serde_json::json!({ "topic": "udp_ports" });
let output = inspect_host(&args).await.expect("udp_ports must return Ok");
assert!(output.contains("Host inspection: udp_ports"));
});
}
#[test]
fn test_routing_detects_udp_ports_topic() {
use hematite::agent::routing::preferred_host_inspection_topic;
assert_eq!(preferred_host_inspection_topic("what is listening on UDP?"), Some("udp_ports"));
assert_eq!(preferred_host_inspection_topic("show open UDP ports"), Some("udp_ports"));
}
#[tokio::test]
async fn test_inspect_host_storage_includes_latency() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "storage" }))
.await
.expect("inspect storage fails");
assert!(output.contains("Real-time Disk Intensity:"));
assert!(output.contains("Average Disk Queue Length:"));
}
#[tokio::test]
async fn test_inspect_host_sessions() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "sessions" }))
.await
.expect("inspect sessions fails");
assert!(output.contains("Host inspection: sessions"));
assert!(
output.contains("Active Logon Sessions")
|| output.contains("Logged-in Users")
);
}
#[tokio::test]
async fn test_inspect_host_hardware_expanded() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "hardware" }))
.await
.expect("inspect hardware fails");
assert!(output.contains("Motherboard:"));
assert!(output.contains("BIOS:"));
assert!(output.contains("Virtualization:"));
assert!(
output.contains("Hypervisor:")
|| output.contains("unsupported")
);
}
#[tokio::test]
async fn test_inspect_host_processes_io() {
use serde_json::json;
let output = hematite::tools::host_inspect::inspect_host(&json!({ "topic": "processes", "max_entries": 1 }))
.await
.expect("inspect processes fails");
assert!(output.contains("Top processes by resource usage:"));
assert!(
output.contains("I/O R:")
|| output.contains("unknown")
);
}