use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use serde_json::{Value, json};
use tempfile::TempDir;
use tower::ServiceExt;
use trusty_common::error_capture::CapturedError;
use trusty_common::uds::server::{
CODE_INTERNAL_ERROR, CODE_INVALID_PARAMS, CODE_METHOD_NOT_FOUND, RpcResponse, RpcRouter,
};
use super::{METHODS, register};
use crate::core::paths::FrameworkPaths;
use crate::daemon::api;
use crate::daemon::bug_report;
use crate::daemon::error::{CODE_NOT_FOUND, CODE_UNAVAILABLE};
use crate::daemon::state::DaemonState;
fn hermetic() -> (Arc<DaemonState>, TempDir) {
let dir = tempfile::tempdir().expect("temp dir for hermetic DaemonState");
let paths = FrameworkPaths::under(dir.path());
(Arc::new(DaemonState::with_paths(&paths)), dir)
}
fn rpc_router(state: &Arc<DaemonState>) -> RpcRouter {
register(RpcRouter::new(), state)
}
struct HomeGuard {
home: Option<std::ffi::OsString>,
opt_in: Option<std::ffi::OsString>,
}
impl Drop for HomeGuard {
fn drop(&mut self) {
unsafe {
match self.home.take() {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match self.opt_in.take() {
Some(v) => std::env::set_var(crate::core::host_state_gate::ALLOW_HOST_STATE_ENV, v),
None => {
std::env::remove_var(crate::core::host_state_gate::ALLOW_HOST_STATE_ENV);
}
}
}
}
}
fn pinned_home() -> (TempDir, HomeGuard) {
let home = tempfile::tempdir().expect("temp dir for a pinned $HOME");
let guard = HomeGuard {
home: std::env::var_os("HOME"),
opt_in: std::env::var_os(crate::core::host_state_gate::ALLOW_HOST_STATE_ENV),
};
unsafe {
std::env::set_var("HOME", home.path());
std::env::remove_var(crate::core::host_state_gate::ALLOW_HOST_STATE_ENV);
}
(home, guard)
}
async fn http(
state: &Arc<DaemonState>,
method: &str,
uri: &str,
body: Option<Value>,
) -> (StatusCode, Value) {
let request = Request::builder().method(method).uri(uri);
let request = match body {
Some(v) => request
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&v).expect("encode body")))
.expect("build request"),
None => request.body(Body::empty()).expect("build request"),
};
let response = api::router(Arc::clone(state))
.oneshot(request)
.await
.expect("the router must answer");
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("read the response body");
let value = if bytes.is_empty() {
Value::Null
} else {
serde_json::from_slice(&bytes).expect("the HTTP body must be JSON")
};
(status, value)
}
async fn rpc_frame(router: &RpcRouter, method: &str, params: Value) -> Value {
let frame = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params });
let response: RpcResponse = router
.dispatch(&serde_json::to_vec(&frame).expect("encode the request frame"))
.await;
serde_json::to_value(response).expect("the response frame must serialise")
}
async fn rpc_ok(router: &RpcRouter, method: &str, params: Value) -> Value {
let frame = rpc_frame(router, method, params).await;
assert!(
frame.get("error").is_none(),
"{method} must succeed, got {frame}"
);
frame
.get("result")
.cloned()
.unwrap_or_else(|| panic!("{method} answered no result: {frame}"))
}
async fn rpc_err(router: &RpcRouter, method: &str, params: Value) -> Value {
let frame = rpc_frame(router, method, params).await;
frame
.get("error")
.cloned()
.unwrap_or_else(|| panic!("{method} must be refused, got {frame}"))
}
fn assert_same(method: &str, mut http_body: Value, mut rpc_result: Value, drop_fields: &[&str]) {
for field in drop_fields {
if let Some(map) = http_body.as_object_mut() {
map.remove(*field);
}
if let Some(map) = rpc_result.as_object_mut() {
map.remove(*field);
}
}
assert_eq!(
http_body, rpc_result,
"{method} must answer identically over HTTP and the socket"
);
}
#[tokio::test]
async fn rpc_router_registers_every_documented_method() {
let (state, _dir) = hermetic();
let router = rpc_router(&state);
let registered: Vec<&str> = router.method_names().collect();
let mut documented: Vec<&str> = METHODS.to_vec();
documented.sort_unstable();
assert_eq!(
registered, documented,
"the router and METHODS must name the same set"
);
assert_eq!(
METHODS.len(),
20,
"slice 2 owns twenty routes; a new one needs a row in core.rs's table too"
);
}
#[tokio::test]
async fn rpc_reports_method_not_found_for_an_unknown_method() {
let (state, _dir) = hermetic();
let error = rpc_err(&rpc_router(&state), "mpm.no.such.method", json!({})).await;
assert_eq!(error["code"], json!(CODE_METHOD_NOT_FOUND), "{error}");
}
#[tokio::test]
async fn rpc_health_answers_with_no_params() {
let (state, _dir) = hermetic();
let result = rpc_ok(&rpc_router(&state), "mpm.health", Value::Null).await;
assert_eq!(result["status"], json!("ok"), "{result}");
}
#[tokio::test]
async fn rpc_health_answers_with_a_stray_params_object() {
let (state, _dir) = hermetic();
let result = rpc_ok(&rpc_router(&state), "mpm.health", json!({"unknown": 1})).await;
assert_eq!(result["status"], json!("ok"), "{result}");
}
#[tokio::test]
async fn rpc_reports_invalid_params_for_an_undecodable_payload() {
let (state, _dir) = hermetic();
let error = rpc_err(
&rpc_router(&state),
"mpm.tmux.snapshot",
json!({"name": 42}),
)
.await;
assert_eq!(error["code"], json!(CODE_INVALID_PARAMS), "{error}");
}
#[tokio::test]
async fn parity_health_agrees_across_transports() {
let (state, _dir) = hermetic();
crate::daemon::rpc::health_catalog::report_within(&state, std::time::Duration::from_secs(30))
.await;
let (status, body) = http(&state, "GET", "/health", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(&rpc_router(&state), "mpm.health", Value::Null).await;
assert_same("mpm.health", body, result, &[]);
}
#[tokio::test]
async fn parity_breakers_agrees_across_transports() {
let (state, _dir) = hermetic();
let (status, body) = http(&state, "GET", "/breakers", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(&rpc_router(&state), "mpm.breakers", Value::Null).await;
assert_same("mpm.breakers", body, result, &[]);
}
#[tokio::test]
async fn parity_optimizer_agrees_across_transports() {
let (state, _dir) = hermetic();
let (status, body) = http(&state, "GET", "/optimizer", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(&rpc_router(&state), "mpm.optimizer", Value::Null).await;
assert_same("mpm.optimizer", body, result, &[]);
}
#[tokio::test]
async fn parity_overseer_agrees_across_transports() {
let (state, _dir) = hermetic();
let (status, body) = http(&state, "GET", "/overseer", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(&rpc_router(&state), "mpm.overseer", Value::Null).await;
assert_same("mpm.overseer", body, result, &[]);
}
fn seed_one_error(state: &Arc<DaemonState>) -> String {
let base = state
.error_store_base()
.expect("a hermetic daemon must pin its error-store base");
let path = bug_report::store_paths_under(base)
.into_iter()
.find(|p| p.ends_with("trusty-mpm/errors.jsonl"))
.expect("the aggregated store set must include this crate's own daemon");
std::fs::create_dir_all(path.parent().expect("the store path has a parent"))
.expect("create the hermetic store directory");
let record = CapturedError {
timestamp_secs: 1_700_000_000,
crate_target: "trusty_mpm::daemon::rpc".to_string(),
crate_version: "0.0.0-test".to_string(),
message: "seeded parity record".to_string(),
fields: String::new(),
file: Some("src/daemon/rpc/core_tests.rs".to_string()),
line: Some(1),
os: "test-os".to_string(),
arch: "test-arch".to_string(),
fingerprint: "6505000000000000000000000000000000000000000000000000000000000000".to_string(),
};
std::fs::write(
&path,
format!(
"{}\n",
serde_json::to_string(&record).expect("encode record")
),
)
.expect("write the hermetic error store");
record.fingerprint
}
#[tokio::test]
async fn parity_errors_agrees_across_transports() {
let (state, _dir) = hermetic();
let fingerprint = seed_one_error(&state);
let (status, body) = http(&state, "GET", "/api/v1/errors?limit=5", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(&rpc_router(&state), "mpm.errors.list", json!({"limit": 5})).await;
assert_eq!(body["limit"], json!(5), "the argument must reach the body");
assert_eq!(
body["total"],
json!(1),
"the hermetic store holds one: {body}"
);
assert_eq!(
body["errors"][0]["fingerprint"],
json!(fingerprint),
"{body}"
);
assert_same("mpm.errors.list", body, result, &[]);
}
#[serial_test::serial]
#[tokio::test]
async fn parity_tmux_sessions_agrees_across_transports() {
let (_home, _home_guard) = pinned_home();
let (state, _dir) = hermetic();
let (status, body) = http(&state, "GET", "/tmux/sessions", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(&rpc_router(&state), "mpm.tmux.sessions", Value::Null).await;
assert_eq!(
body["sessions"],
json!([]),
"a scratch $HOME must reach no host tmux session: {body}"
);
assert_same("mpm.tmux.sessions", body, result, &[]);
}
#[serial_test::serial]
#[tokio::test]
async fn parity_doctor_agrees_across_transports() {
let (_home, _home_guard) = pinned_home();
let (state, dir) = hermetic();
let project = dir.path().display().to_string();
let (status, body) = http(
&state,
"GET",
&format!("/api/v1/doctor?project={project}"),
None,
)
.await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(
&rpc_router(&state),
"mpm.doctor",
json!({"project": project}),
)
.await;
assert!(
body.get("generated_at").is_some(),
"the allowlist entry must name a field that exists: {body}"
);
assert_same(
"mpm.doctor",
normalize_host_sampled_checks(body),
normalize_host_sampled_checks(result),
&["generated_at"],
);
}
const HOST_SAMPLED_CHECKS: &[&str] = &[
"worktree_disk",
"worktrees",
"session_store",
"pty_headroom",
"skill_staleness",
"disk_usage",
];
fn normalize_host_sampled_checks(mut report: Value) -> Value {
let mut normalized: Vec<String> = Vec::new();
if let Some(checks) = report.get_mut("checks").and_then(Value::as_array_mut) {
for check in checks {
let Some(name) = check.get("name").and_then(Value::as_str).map(str::to_owned) else {
continue;
};
if HOST_SAMPLED_CHECKS.contains(&name.as_str())
&& let Some(map) = check.as_object_mut()
{
assert!(
map.get("status")
.and_then(Value::as_str)
.is_some_and(|s| !s.is_empty()),
"the `{name}` check must still report a status — normalizing \
its VALUE is what this allowlist buys, dropping the field is not"
);
map.insert("message".into(), json!("<host-sampled>"));
map.insert("status".into(), json!("<host-sampled>"));
normalized.push(name);
}
}
}
for name in HOST_SAMPLED_CHECKS {
assert!(
normalized.iter().any(|seen| seen == name),
"the allowlist names the `{name}` check, which this report does not \
contain — a renamed check must fail here rather than silently stop \
being excluded"
);
}
report
}
#[test]
fn host_sampled_status_is_excused_from_parity_and_no_other_check_is() {
let report = |worktree_disk_status: &str, instructions_status: &str| {
json!({
"checks": [
{"name": "worktree_disk", "status": worktree_disk_status, "message": "6.2 GiB measured"},
{"name": "worktrees", "status": "ok", "message": "14 live, 265 not reclaimable"},
{"name": "session_store", "status": "ok", "message": "43 session record(s), 59773 byte(s)"},
{"name": "pty_headroom", "status": "ok", "message": "82 of 511 pseudo-terminals allocated"},
{"name": "skill_staleness", "status": "ok", "message": "every deployed skill matches"},
{"name": "disk_usage", "status": "ok", "message": "/ is at 79.6% disk usage"},
{"name": "instructions", "status": instructions_status, "message": "last-instructions.md not found"},
]
})
};
let http = normalize_host_sampled_checks(report("unknown", "warn"));
let socket = normalize_host_sampled_checks(report("warn", "warn"));
assert_eq!(
http, socket,
"a host-sampled check whose status churned between the reads must not fail parity"
);
let http = normalize_host_sampled_checks(report("warn", "ok"));
let socket = normalize_host_sampled_checks(report("warn", "fail"));
assert_ne!(
http, socket,
"a NON-host-sampled check's STATUS divergence must still fail parity"
);
}
#[test]
fn skill_staleness_divergence_is_excused_from_parity_and_no_other_check_is() {
let report = |skills_status: &str, skills_message: &str, instructions_status: &str| {
json!({
"checks": [
{"name": "worktree_disk", "status": "warn", "message": "6.2 GiB measured"},
{"name": "worktrees", "status": "ok", "message": "14 live, 265 not reclaimable"},
{"name": "session_store", "status": "ok", "message": "43 session record(s)"},
{"name": "pty_headroom", "status": "ok", "message": "82 of 511 allocated"},
{"name": "skill_staleness", "status": skills_status, "message": skills_message},
{"name": "disk_usage", "status": "ok", "message": "/ is at 79.6% disk usage"},
{"name": "instructions", "status": instructions_status, "message": "last-instructions.md not found"},
]
})
};
let http = normalize_host_sampled_checks(report(
"warn",
"2 drifted at the operator home tier — REPAIRABLE by `tm install`",
"warn",
));
let socket = normalize_host_sampled_checks(report(
"ok",
"every deployed skill matches this binary's bundled assets",
"warn",
));
assert_eq!(
http, socket,
"a `skill_staleness` row that churned between the reads must not fail parity"
);
let http = normalize_host_sampled_checks(report("ok", "clean", "ok"));
let socket = normalize_host_sampled_checks(report("ok", "clean", "fail"));
assert_ne!(
http, socket,
"a NON-host-sampled check's STATUS divergence must still fail parity"
);
}
#[test]
fn disk_usage_divergence_is_excused_from_parity_and_no_other_check_is() {
let report = |disk_status: &str, disk_message: &str, instructions_status: &str| {
json!({
"checks": [
{"name": "worktree_disk", "status": "warn", "message": "6.2 GiB measured"},
{"name": "worktrees", "status": "ok", "message": "14 live, 265 not reclaimable"},
{"name": "session_store", "status": "ok", "message": "43 session record(s)"},
{"name": "pty_headroom", "status": "ok", "message": "82 of 511 allocated"},
{"name": "skill_staleness", "status": "ok", "message": "every deployed skill matches"},
{"name": "disk_usage", "status": disk_status, "message": disk_message},
{"name": "instructions", "status": instructions_status, "message": "last-instructions.md not found"},
]
})
};
let http = normalize_host_sampled_checks(report(
"ok",
"/System/Volumes/Data is at 79.6% disk usage; disk.max_usage_pct threshold is 90%",
"warn",
));
let socket = normalize_host_sampled_checks(report(
"ok",
"/System/Volumes/Data is at 79.7% disk usage; disk.max_usage_pct threshold is 90%",
"warn",
));
assert_eq!(
http, socket,
"a `disk_usage` row resampled between the reads must not fail parity"
);
let http = normalize_host_sampled_checks(report("ok", "at 89.9%", "warn"));
let socket = normalize_host_sampled_checks(report("warn", "at 90.0%", "warn"));
assert_eq!(
http, socket,
"a `disk_usage` row whose verdict crossed the threshold between the reads \
must not fail parity"
);
let http = normalize_host_sampled_checks(report("ok", "at 79.6%", "ok"));
let socket = normalize_host_sampled_checks(report("ok", "at 79.6%", "fail"));
assert_ne!(
http, socket,
"a NON-host-sampled check's STATUS divergence must still fail parity"
);
}
#[test]
#[should_panic(expected = "must still report a status")]
fn normalizing_a_host_sampled_check_requires_it_to_report_a_status() {
normalize_host_sampled_checks(json!({
"checks": [
{"name": "worktree_disk", "message": "6.2 GiB measured"},
{"name": "worktrees", "status": "ok", "message": "14 live"},
{"name": "session_store", "status": "ok", "message": "43 record(s)"},
{"name": "pty_headroom", "status": "ok", "message": "82 of 511"},
{"name": "skill_staleness", "status": "ok", "message": "clean"},
]
}));
}
#[tokio::test]
async fn parity_report_bug_preview_agrees_across_transports() {
let (state, _dir) = hermetic();
let payload = json!({
"fingerprint": "0".repeat(64),
"confirm": false,
});
let (status, body) = http(&state, "POST", "/api/v1/report-bug", Some(payload.clone())).await;
assert_eq!(status, StatusCode::OK, "this route is always 200");
assert_eq!(body["filed"], json!(false), "nothing may be filed: {body}");
let result = rpc_ok(&rpc_router(&state), "mpm.report_bug", payload).await;
assert_same("mpm.report_bug", body, result, &[]);
}
#[serial_test::serial]
#[tokio::test]
async fn parity_claude_config_get_agrees_across_transports() {
let (_home, _home_guard) = pinned_home();
let (state, dir) = hermetic();
let project = dir.path().display().to_string();
let (status, body) = http(
&state,
"GET",
&format!("/claude-config?project={project}"),
None,
)
.await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(
&rpc_router(&state),
"mpm.claude_config.get",
json!({"project": project}),
)
.await;
assert_same("mpm.claude_config.get", body, result, &[]);
}
#[tokio::test]
async fn parity_claude_config_checkpoints_agrees_across_transports() {
let (state, dir) = hermetic();
let project = dir.path().display().to_string();
let (status, body) = http(
&state,
"GET",
&format!("/claude-config/checkpoints?project={project}"),
None,
)
.await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(
&rpc_router(&state),
"mpm.claude_config.checkpoints.list",
json!({"project": project}),
)
.await;
assert_same("mpm.claude_config.checkpoints.list", body, result, &[]);
}
#[tokio::test]
async fn parity_claude_config_profiles_agrees_across_transports() {
let (state, _dir) = hermetic();
let (status, body) = http(&state, "GET", "/claude-config/profiles", None).await;
assert_eq!(status, StatusCode::OK);
let result = rpc_ok(
&rpc_router(&state),
"mpm.claude_config.profiles",
Value::Null,
)
.await;
assert_same("mpm.claude_config.profiles", body, result, &[]);
}
#[tokio::test]
async fn rpc_llm_chat_without_overseer_reports_unavailable() {
let (state, _dir) = hermetic();
let payload = json!({"message": "hello", "history": []});
let (status, body) = http(&state, "POST", "/llm/chat", Some(payload.clone())).await;
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
let error = rpc_err(&rpc_router(&state), "mpm.llm.chat", payload).await;
assert_eq!(error["code"], json!(CODE_UNAVAILABLE), "{error}");
assert_eq!(
error["message"], body["error"],
"the socket must carry the HTTP body's message verbatim"
);
}
#[tokio::test]
async fn rpc_claude_config_deploy_unknown_profile_reports_not_found() {
let (state, dir) = hermetic();
let payload = json!({
"project": dir.path().display().to_string(),
"profile_name": "no-such-profile-xyz",
});
let (status, _body) = http(
&state,
"POST",
"/claude-config/deploy",
Some(payload.clone()),
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND, "HTTP must still 404");
let error = rpc_err(&rpc_router(&state), "mpm.claude_config.deploy", payload).await;
assert_eq!(error["code"], json!(CODE_NOT_FOUND), "{error}");
assert!(
error["message"]
.as_str()
.unwrap_or_default()
.contains("no-such-profile-xyz"),
"the refusal must name what was not found: {error}"
);
}
#[tokio::test]
async fn rpc_claude_config_apply_unknown_recommendation_reports_not_found() {
let (state, dir) = hermetic();
let payload = json!({
"project": dir.path().display().to_string(),
"recommendation_id": "no-such-recommendation-xyz",
});
let (status, _body) = http(
&state,
"POST",
"/claude-config/apply",
Some(payload.clone()),
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND, "HTTP must still 404");
let error = rpc_err(&rpc_router(&state), "mpm.claude_config.apply", payload).await;
assert_eq!(error["code"], json!(CODE_NOT_FOUND), "{error}");
}
#[serial_test::serial]
#[tokio::test]
async fn rpc_tmux_snapshot_unknown_session_reports_a_coded_error() {
let (_home, _home_guard) = pinned_home();
let (state, _dir) = hermetic();
let (status, body) = http(
&state,
"GET",
"/tmux/sessions/no-such-session-xyz/snapshot",
None,
)
.await;
assert!(
status.is_client_error() || status.is_server_error(),
"{status}"
);
let error = rpc_err(
&rpc_router(&state),
"mpm.tmux.snapshot",
json!({"name": "no-such-session-xyz"}),
)
.await;
assert_eq!(
error["message"], body["error"],
"the socket must carry the HTTP body's message verbatim"
);
let expected = match status {
StatusCode::NOT_FOUND => CODE_NOT_FOUND,
StatusCode::INTERNAL_SERVER_ERROR => trusty_common::uds::server::CODE_INTERNAL_ERROR,
other => panic!("unexpected status for a missing tmux session: {other}"),
};
assert_eq!(error["code"], json!(expected), "{error}");
}
#[serial_test::serial]
#[tokio::test]
async fn parity_tmux_adopt_unknown_session_agrees_across_transports() {
let (state, _dir) = hermetic();
let payload = json!({"session": "no-such-session-xyz"});
let (status, body) = http(&state, "POST", "/tmux/adopt", Some(payload.clone())).await;
assert!(
status.is_client_error() || status.is_server_error(),
"{status}"
);
let error = rpc_err(&rpc_router(&state), "mpm.tmux.adopt", payload).await;
assert_eq!(
error["message"], body["error"],
"the socket must carry the HTTP body's message verbatim"
);
}
#[tokio::test]
async fn rpc_claude_config_checkpoint_create_then_list_sees_it() {
let (state, _dir) = hermetic();
let project = tempfile::tempdir().expect("project dir");
let project_path = project.path().display().to_string();
let router = rpc_router(&state);
let created = rpc_ok(
&router,
"mpm.claude_config.checkpoints.create",
json!({"project": project_path, "label": "from-the-socket"}),
)
.await;
let id = created["id"]
.as_str()
.expect("create returns an id")
.to_string();
assert!(!id.is_empty(), "the id must not be empty: {created}");
let listed = rpc_ok(
&router,
"mpm.claude_config.checkpoints.list",
json!({"project": project_path}),
)
.await;
assert!(
checkpoint_ids(&listed).contains(&id),
"the checkpoint written over the socket must be listed back: {listed}"
);
}
#[tokio::test]
async fn rpc_claude_config_checkpoint_delete_removes_a_created_checkpoint() {
let (state, _dir) = hermetic();
let project = tempfile::tempdir().expect("project dir");
let project_path = project.path().display().to_string();
let router = rpc_router(&state);
let created = rpc_ok(
&router,
"mpm.claude_config.checkpoints.create",
json!({"project": project_path}),
)
.await;
let id = created["id"]
.as_str()
.expect("create returns an id")
.to_string();
assert!(
checkpoint_ids(
&rpc_ok(
&router,
"mpm.claude_config.checkpoints.list",
json!({"project": project_path}),
)
.await
)
.contains(&id),
"the checkpoint must exist before the delete is meaningful"
);
let deleted = rpc_ok(
&router,
"mpm.claude_config.checkpoints.delete",
json!({"project": project_path, "id": id}),
)
.await;
assert_eq!(
deleted["deleted"],
json!(id),
"delete must name the checkpoint it removed: {deleted}"
);
let listed = rpc_ok(
&router,
"mpm.claude_config.checkpoints.list",
json!({"project": project_path}),
)
.await;
assert!(
!checkpoint_ids(&listed).contains(&id),
"the deleted checkpoint must be gone from the listing: {listed}"
);
}
fn checkpoint_ids(listed: &Value) -> Vec<String> {
listed["checkpoints"]
.as_array()
.expect("checkpoints is an array")
.iter()
.filter_map(|c| c["id"].as_str().map(str::to_owned))
.collect()
}
#[tokio::test]
async fn rpc_claude_config_delete_unknown_checkpoint_reports_not_found() {
let (state, _dir) = hermetic();
let project = tempfile::tempdir().expect("project dir");
let project_path = project.path().display().to_string();
let (status, _body) = http(
&state,
"DELETE",
&format!("/claude-config/checkpoints/no-such-checkpoint?project={project_path}"),
None,
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND, "HTTP must still 404");
let error = rpc_err(
&rpc_router(&state),
"mpm.claude_config.checkpoints.delete",
json!({"project": project_path, "id": "no-such-checkpoint"}),
)
.await;
assert_eq!(error["code"], json!(CODE_NOT_FOUND), "{error}");
}
#[tokio::test]
async fn rpc_claude_config_restore_unknown_checkpoint_reports_internal_error() {
let (state, _dir) = hermetic();
let project = tempfile::tempdir().expect("project dir");
let payload = json!({
"project": project.path().display().to_string(),
"checkpoint_id": "no-such-checkpoint",
});
let (status, _body) = http(
&state,
"POST",
"/claude-config/restore",
Some(payload.clone()),
)
.await;
assert_eq!(
status,
StatusCode::INTERNAL_SERVER_ERROR,
"HTTP must still 500"
);
let error = rpc_err(&rpc_router(&state), "mpm.claude_config.restore", payload).await;
assert_eq!(error["code"], json!(CODE_INTERNAL_ERROR), "{error}");
}
#[tokio::test]
async fn rpc_claude_config_restart_unknown_session_reports_a_coded_error() {
let (state, _dir) = hermetic();
let payload = json!({"tmux_session": "no-such-session-xyz"});
let (status, _body) = http(
&state,
"POST",
"/claude-config/restart",
Some(payload.clone()),
)
.await;
assert!(
status.is_client_error() || status.is_server_error(),
"restarting a session that is not there must fail: {status}"
);
let error = rpc_err(&rpc_router(&state), "mpm.claude_config.restart", payload).await;
let expected = match status {
StatusCode::NOT_FOUND => CODE_NOT_FOUND,
StatusCode::INTERNAL_SERVER_ERROR => CODE_INTERNAL_ERROR,
other => panic!("unexpected status for a missing tmux session: {other}"),
};
assert_eq!(error["code"], json!(expected), "{error}");
}