use super::*;
fn write_mcp_json(project: &Path, args: &serde_json::Value) {
let body = serde_json::json!({
"mcpServers": {
"trusty-memory": {"type": "stdio", "command": "trusty-memory", "args": ["serve"]},
"trusty-search": {"type": "stdio", "command": "trusty-search", "args": args},
}
});
std::fs::write(
project.join(".mcp.json"),
serde_json::to_string_pretty(&body).unwrap(),
)
.unwrap();
}
async fn spawn_status_daemon(
answer: Result<serde_json::Value, crate::uds_mock::RpcError>,
) -> crate::uds_mock::MockUdsDaemon {
crate::uds_mock::spawn(move |_method: &str, _params: serde_json::Value| {
let answer = answer.clone();
Box::pin(async move { answer })
})
.await
}
#[test]
fn read_pin_finds_the_pinned_id() {
let tmp = tempfile::tempdir().unwrap();
write_mcp_json(
tmp.path(),
&serde_json::json!(["serve", "--index", "trusty-tools"]),
);
assert_eq!(
read_pinned_index_id(tmp.path()),
PinState::Pinned("trusty-tools".to_string())
);
}
#[test]
fn read_pin_reports_unpinned_stub() {
let tmp = tempfile::tempdir().unwrap();
write_mcp_json(tmp.path(), &serde_json::json!(["serve"]));
assert_eq!(read_pinned_index_id(tmp.path()), PinState::Unpinned);
}
#[test]
fn read_pin_treats_a_blank_id_as_unpinned() {
let tmp = tempfile::tempdir().unwrap();
write_mcp_json(tmp.path(), &serde_json::json!(["serve", "--index", " "]));
assert_eq!(read_pinned_index_id(tmp.path()), PinState::Unpinned);
}
#[test]
fn read_pin_reports_missing_file() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(read_pinned_index_id(tmp.path()), PinState::NoMcpJson);
}
#[test]
fn read_pin_reports_missing_server() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join(".mcp.json"),
r#"{"mcpServers":{"trusty-memory":{"args":["serve"]}}}"#,
)
.unwrap();
assert_eq!(read_pinned_index_id(tmp.path()), PinState::NoSearchServer);
}
#[test]
fn read_pin_reports_unreadable_json() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".mcp.json"), "{not json").unwrap();
assert!(matches!(
read_pinned_index_id(tmp.path()),
PinState::Unreadable(_)
));
}
#[tokio::test]
async fn probe_reports_unknown_index_when_the_daemon_says_not_found() {
let daemon = spawn_status_daemon(Err(crate::uds_mock::RpcError::new(
crate::daemon::error::CODE_NOT_FOUND,
"unknown index",
)))
.await;
assert_eq!(
probe_pinned_index(daemon.socket(), "ghost-index").await,
PinProbe::UnknownIndex
);
}
#[tokio::test]
async fn probe_reports_resolved_on_a_status_result() {
let daemon = spawn_status_daemon(Ok(serde_json::json!({"chunk_count": 4210}))).await;
assert_eq!(
probe_pinned_index(daemon.socket(), "trusty-tools").await,
PinProbe::Resolved {
chunk_count: Some(4210)
}
);
}
#[tokio::test]
async fn probe_reports_another_refusal_as_an_rpc_code() {
let daemon = spawn_status_daemon(Err(crate::uds_mock::RpcError::internal("boom"))).await;
assert!(matches!(
probe_pinned_index(daemon.socket(), "trusty-tools").await,
PinProbe::RpcRefusal(_)
));
}
#[tokio::test]
async fn probe_reports_unreachable_when_nothing_listens() {
assert!(matches!(
probe_pinned_index(
std::path::Path::new("/nonexistent/trusty-search/ts.sock"),
"trusty-tools"
)
.await,
PinProbe::Unreachable(_)
));
}
#[test]
fn build_pin_check_fails_on_unknown_index() {
let check = build_pin_check(
&PinState::Pinned("trusty-tools".to_string()),
Some(&PinProbe::UnknownIndex),
);
assert_eq!(check.status, CheckStatus::Fail);
assert!(check.message.contains("trusty-tools"));
assert!(
check.message.contains("#5045"),
"the failure must cite the issue: {}",
check.message
);
}
#[test]
fn build_pin_check_warns_on_empty_index() {
let check = build_pin_check(
&PinState::Pinned("trusty-tools".to_string()),
Some(&PinProbe::Resolved {
chunk_count: Some(0),
}),
);
assert_eq!(check.status, CheckStatus::Warn);
}
#[test]
fn build_pin_check_ok_when_index_resolves() {
let check = build_pin_check(
&PinState::Pinned("trusty-tools".to_string()),
Some(&PinProbe::Resolved {
chunk_count: Some(4210),
}),
);
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn build_pin_check_unreachable_is_unknown_not_ok() {
let check = build_pin_check(
&PinState::Pinned("trusty-tools".to_string()),
Some(&PinProbe::Unreachable("connection refused".to_string())),
);
assert_eq!(check.status, CheckStatus::Unknown);
}
#[test]
fn build_pin_check_fails_on_another_refusal() {
let check = build_pin_check(
&PinState::Pinned("trusty-tools".to_string()),
Some(&PinProbe::RpcRefusal(-32603)),
);
assert_eq!(check.status, CheckStatus::Fail);
}
#[test]
fn build_pin_check_warns_on_unpinned_stub() {
let check = build_pin_check(&PinState::Unpinned, None);
assert_eq!(check.status, CheckStatus::Warn);
}
#[test]
fn build_pin_check_warns_on_unreadable_mcp_json() {
let check = build_pin_check(&PinState::Unreadable("bad".to_string()), None);
assert_eq!(check.status, CheckStatus::Warn);
}
#[test]
fn build_pin_check_ok_when_nothing_is_pinned() {
assert_eq!(
build_pin_check(&PinState::NoMcpJson, None).status,
CheckStatus::Ok
);
assert_eq!(
build_pin_check(&PinState::NoSearchServer, None).status,
CheckStatus::Ok
);
}
#[tokio::test]
#[serial_test::serial]
async fn pinned_but_missing_index_is_fail() {
let daemon = spawn_status_daemon(Err(crate::uds_mock::RpcError::new(
crate::daemon::error::CODE_NOT_FOUND,
"unknown index",
)))
.await;
let project = tempfile::tempdir().unwrap();
write_mcp_json(
project.path(),
&serde_json::json!(["serve", "--index", "2eb72dca-de08-481b-8dfa-22ab7f81b1f9"]),
);
let home = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var(search_rpc::TRUSTY_SEARCH_SOCKET_ENV, daemon.socket());
}
let check = check_search_index_pin(home.path(), Some(project.path())).await;
unsafe {
std::env::remove_var(search_rpc::TRUSTY_SEARCH_SOCKET_ENV);
}
assert_eq!(
check.status,
CheckStatus::Fail,
"a pinned-but-nonexistent index must FAIL, not report healthy: {}",
check.message
);
assert!(
check
.message
.contains("2eb72dca-de08-481b-8dfa-22ab7f81b1f9")
);
}
#[tokio::test]
async fn unscoped_run_warns_rather_than_reporting_ok() {
let home = tempfile::tempdir().unwrap();
let check = check_search_index_pin(home.path(), None).await;
assert_eq!(check.status, CheckStatus::Warn);
}