use super::*;
struct MockBackend;
#[async_trait]
impl OrchestratorBackend for MockBackend {
async fn session_list(&self) -> Result<Value, String> {
Ok(json!([{ "id": "s1", "status": "active" }]))
}
async fn session_status(&self, session_id: &str) -> Result<Value, String> {
Ok(json!({ "id": session_id, "status": "active" }))
}
async fn agent_delegate(
&self,
_session_id: &str,
agent: &str,
_task: &str,
_tier: Option<&str>,
) -> Result<Value, String> {
Ok(json!({ "delegation_id": "d1", "agent": agent }))
}
async fn memory_protect(
&self,
_session_id: &str,
used_tokens: u64,
window_tokens: u64,
) -> Result<Value, String> {
Ok(json!({ "fraction": used_tokens as f64 / window_tokens as f64 }))
}
async fn circuit_breaker_status(&self, _agent: Option<&str>) -> Result<Value, String> {
Ok(json!({ "state": "closed" }))
}
async fn hook_event(
&self,
_session_id: &str,
event: &str,
_payload: Value,
) -> Result<Value, String> {
Ok(json!({ "received": event }))
}
async fn list_recent_errors(&self, limit: u64) -> Result<Value, String> {
Ok(json!({ "errors": [], "limit": limit, "total": 0 }))
}
async fn preview_bug_report(&self, fingerprint: &str) -> Result<Value, String> {
Ok(json!({ "fingerprint": fingerprint, "title": "mock title", "body": "mock body" }))
}
async fn report_bug(&self, fingerprint: &str, confirm: bool) -> Result<Value, String> {
if confirm {
Ok(json!({ "filed": false, "note": "mock: no token in test" }))
} else {
Ok(
json!({ "filed": false, "note": "confirm:false — preview only", "fingerprint": fingerprint }),
)
}
}
async fn session_new(
&self,
repo_url: &str,
git_ref: &str,
_task: &str,
_name_hint: Option<&str>,
runtime: Option<&str>,
ephemeral: Option<bool>,
) -> Result<Value, String> {
Ok(json!({
"id": "m-1",
"repo_url": repo_url,
"branch": git_ref,
"runtime": runtime.unwrap_or("claude-code"),
"ephemeral": ephemeral.unwrap_or(false),
"state": "active",
}))
}
async fn session_stop(&self, session_id: &str) -> Result<Value, String> {
Ok(json!({ "id": session_id, "state": "stopped" }))
}
async fn session_resume(&self, session_id: &str) -> Result<Value, String> {
Ok(json!({ "id": session_id, "state": "active" }))
}
async fn session_decommission(&self, session_id: &str) -> Result<Value, String> {
Ok(json!({ "id": session_id, "state": "decommissioned" }))
}
async fn session_delete(&self, session_id: &str, force: bool) -> Result<Value, String> {
if !force && session_id == "running-id" {
return Err(format!(
"session is active — stop it first, or pass --force ({session_id})"
));
}
Ok(json!({ "id": session_id, "state": "stopped", "deleted": true }))
}
async fn session_activity(&self, session_id: &str, lines: u32) -> Result<Value, String> {
Ok(json!({ "id": session_id, "lines": lines, "raw_pane": "" }))
}
async fn session_send(&self, session_id: &str, text: &str) -> Result<Value, String> {
Ok(json!({ "id": session_id, "sent": true, "text": text }))
}
async fn session_context_catchup(
&self,
project_dir: &str,
session_id: Option<&str>,
all_projects: bool,
full: bool,
) -> Result<Value, String> {
Ok(json!({
"sessions": [],
"recent_commits": [],
"recent_memory": [],
"resolved_snapshot": null,
"watermark_advanced": false,
"project_dir": project_dir,
"session_id": session_id,
"all_projects": all_projects,
"full": full,
}))
}
async fn session_context_pause(
&self,
project_dir: &str,
session_id: &str,
summary: &str,
completed: Vec<String>,
in_progress: Vec<String>,
next_steps: Vec<String>,
tmux_window: Option<&str>,
prune_worktrees: bool,
) -> Result<Value, String> {
Ok(json!({
"snapshot_path": format!("{project_dir}/.trusty-mpm/sessions/session-mock.md"),
"timestamp": "2026-07-20T00:00:00Z",
"pruned_worktrees": [],
"session_id": session_id,
"summary": summary,
"completed": completed,
"in_progress": in_progress,
"next_steps": next_steps,
"tmux_window": tmux_window,
"prune_worktrees": prune_worktrees,
}))
}
async fn session_decommission_ephemeral(&self) -> Result<Value, String> {
Ok(json!({ "decommissioned": 3 }))
}
async fn session_prune(
&self,
state: &str,
dry_run: bool,
include_active: bool,
) -> Result<Value, String> {
crate::session_manager::PruneFilter::parse(state)?;
Ok(json!({
"dry_run": dry_run,
"filter": state,
"include_active": include_active,
"sessions": [],
}))
}
async fn console_metrics(&self) -> Result<Value, String> {
Ok(json!({
"service_id": "trusty-mpm",
"display_name": "Trusty MPM",
"version": "0.0.0",
"status": "ok",
"metrics": { "fleet": { "active": 0 }, "auto_resume": { "desired": false } },
"metrics_schema_version": 1,
}))
}
async fn supervisor_status(&self) -> Result<Value, String> {
Ok(json!({
"fleet": { "active": 0, "stopped": 0, "total": 0 },
"auto_resume": { "desired": false, "env": false, "pending_restart": false },
}))
}
async fn auto_resume_set(&self, enabled: bool) -> Result<Value, String> {
Ok(json!({ "desired": enabled, "env": false, "pending_restart": enabled }))
}
async fn config_read(&self) -> Result<Value, String> {
Ok(json!({
"workspace_root_template": null,
"auto_resume": null,
"default_model": null,
"workspace_root": "/home/test/trusty-mpm-projects",
}))
}
#[allow(clippy::too_many_arguments)]
async fn config_write(
&self,
workspace_root_template: Option<&str>,
auto_resume: Option<bool>,
default_model: Option<&str>,
project_name: Option<&str>,
github_config_dir: Option<&str>,
_github_token_env: Option<&str>,
_github_account: Option<&str>,
_github_host: Option<&str>,
commit_name: Option<&str>,
_commit_email: Option<&str>,
untracked_sync_patterns: Option<Vec<String>>,
untracked_sync_enabled: Option<bool>,
) -> Result<Value, String> {
Ok(json!({
"workspace_root_template": workspace_root_template,
"auto_resume": auto_resume,
"default_model": default_model,
"workspace_root": workspace_root_template.unwrap_or("/home/test/trusty-mpm-projects"),
"project_name": project_name,
"github_config_dir": github_config_dir,
"commit_name": commit_name,
"untracked_sync_patterns": untracked_sync_patterns,
"untracked_sync_enabled": untracked_sync_enabled,
}))
}
async fn project_list(&self) -> Result<Value, String> {
Ok(json!({
"projects": [
{ "name": "trusty-tools", "repo_url": "https://github.com/o/trusty-tools", "default_branch": "main" }
],
"count": 1,
}))
}
async fn project_register(
&self,
name: &str,
repo_url: &str,
default_branch: Option<&str>,
stack_hint: Option<&str>,
tags: Option<Vec<String>>,
description: Option<&str>,
gh_user: Option<&str>,
gh_account: Option<&str>,
) -> Result<Value, String> {
Ok(json!({
"name": name,
"repo_url": repo_url,
"default_branch": default_branch.unwrap_or("main"),
"stack_hint": stack_hint,
"tags": tags.unwrap_or_default(),
"description": description,
"gh_user": gh_user,
"gh_account": gh_account,
}))
}
async fn project_get(&self, name: &str) -> Result<Value, String> {
if name == "trusty-tools" {
Ok(json!({
"name": "trusty-tools",
"repo_url": "https://github.com/o/trusty-tools",
"default_branch": "main",
}))
} else {
Err(format!("project `{name}` not found"))
}
}
async fn session_proxy_focus(
&self,
conversation_key: &str,
session_id: &str,
) -> Result<Value, String> {
Ok(json!({
"outcome": "focused",
"conversation_key": conversation_key,
"session_id": session_id,
}))
}
async fn session_proxy_unfocus(&self, conversation_key: &str) -> Result<Value, String> {
Ok(json!({ "cleared": { "conversation_key": conversation_key } }))
}
async fn session_proxy_message(
&self,
conversation_key: &str,
text: &str,
) -> Result<Value, String> {
Ok(json!({
"outcome": "sent",
"conversation_key": conversation_key,
"text": text,
}))
}
async fn session_proxy_summary(&self, conversation_key: &str) -> Result<Value, String> {
Ok(json!({ "outcome": "summary", "conversation_key": conversation_key }))
}
async fn project_resolve(&self, query: &str) -> Result<Value, String> {
if query == "trusty-tools" {
Ok(json!({
"primary": {
"project": {
"name": "trusty-tools",
"repo_url": "https://github.com/o/trusty-tools",
"default_branch": "main",
},
"confidence": 1.0,
"reason": "exact name match",
},
"needs_disambiguation": false,
"matches": [{
"project": {
"name": "trusty-tools",
"repo_url": "https://github.com/o/trusty-tools",
"default_branch": "main",
},
"confidence": 1.0,
"reason": "exact name match",
}],
}))
} else {
Ok(json!({
"primary": null,
"needs_disambiguation": false,
"matches": [],
"error": format!("no project matched query: {query:?}"),
}))
}
}
}
fn call(name: &str, args: Value) -> Request {
Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(1)),
method: "tools/call".into(),
params: Some(json!({ "name": name, "arguments": args })),
}
}
#[tokio::test]
async fn dispatch_initialize_returns_server_info() {
let req = Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(1)),
method: "initialize".into(),
params: None,
};
let resp = dispatch(&MockBackend, req).await;
let result = resp.result.unwrap();
assert_eq!(result["serverInfo"]["name"], SERVER_NAME);
}
#[tokio::test]
async fn dispatch_tools_list_returns_full_catalog() {
let req = Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(1)),
method: "tools/list".into(),
params: None,
};
let resp = dispatch(&MockBackend, req).await;
let tools = resp.result.unwrap()["tools"].clone();
assert_eq!(tools.as_array().unwrap().len(), 33);
}
#[tokio::test]
async fn dispatch_config_read_tool() {
let resp = dispatch(&MockBackend, call("config_read", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("workspace_root")
);
}
#[tokio::test]
async fn dispatch_config_write_tool() {
let resp = dispatch(
&MockBackend,
call(
"config_write",
json!({ "workspace_root_template": "~/custom-projects" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("custom-projects")
);
}
#[tokio::test]
async fn dispatch_console_metrics_tool() {
let resp = dispatch(&MockBackend, call("console_metrics", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("trusty-mpm")
);
}
#[tokio::test]
async fn dispatch_supervisor_status_tool() {
let resp = dispatch(&MockBackend, call("supervisor_status", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("fleet")
);
}
#[tokio::test]
async fn dispatch_auto_resume_set_tool() {
let resp = dispatch(
&MockBackend,
call("auto_resume_set", json!({ "enabled": true })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("desired")
);
}
#[tokio::test]
async fn dispatch_auto_resume_set_requires_enabled() {
let resp = dispatch(&MockBackend, call("auto_resume_set", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("enabled")
);
}
#[tokio::test]
async fn dispatch_session_list_tool() {
let resp = dispatch(&MockBackend, call("session_list", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("s1")
);
}
#[tokio::test]
async fn dispatch_agent_delegate_tool() {
let resp = dispatch(
&MockBackend,
call(
"agent_delegate",
json!({ "session_id": "s1", "agent": "research", "task": "find" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("research")
);
}
#[tokio::test]
async fn dispatch_missing_argument_is_tool_error() {
let resp = dispatch(&MockBackend, call("session_status", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("session_id")
);
}
#[tokio::test]
async fn dispatch_unknown_tool_is_error() {
let resp = dispatch(&MockBackend, call("nope", json!({}))).await;
assert_eq!(resp.result.unwrap()["isError"], true);
}
#[tokio::test]
async fn dispatch_unknown_method_returns_jsonrpc_error() {
let req = Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(1)),
method: "frobnicate".into(),
params: None,
};
let resp = dispatch(&MockBackend, req).await;
assert_eq!(resp.error.unwrap().code, error_codes::METHOD_NOT_FOUND);
}
#[tokio::test]
async fn dispatch_notification_is_suppressed() {
let req = Request {
jsonrpc: Some("2.0".into()),
id: None,
method: "notifications/initialized".into(),
params: None,
};
let resp = dispatch(&MockBackend, req).await;
assert!(resp.suppress);
}
#[tokio::test]
async fn dispatch_list_recent_errors_tool() {
let resp = dispatch(
&MockBackend,
call("list_recent_errors", json!({ "limit": 10 })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("errors")
);
}
#[tokio::test]
async fn dispatch_list_recent_errors_default_limit() {
let resp = dispatch(&MockBackend, call("list_recent_errors", json!({}))).await;
assert_eq!(resp.result.unwrap()["isError"], false);
}
#[tokio::test]
async fn dispatch_preview_bug_report_tool() {
let fp = "a".repeat(64);
let resp = dispatch(
&MockBackend,
call("preview_bug_report", json!({ "fingerprint": fp })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(result["content"][0]["text"].as_str().unwrap().contains(&fp));
}
#[tokio::test]
async fn dispatch_preview_bug_report_requires_fingerprint() {
let resp = dispatch(&MockBackend, call("preview_bug_report", json!({}))).await;
assert_eq!(resp.result.unwrap()["isError"], true);
}
#[tokio::test]
async fn dispatch_report_bug_no_confirm_is_preview_only() {
let fp = "b".repeat(64);
let resp = dispatch(
&MockBackend,
call("report_bug", json!({ "fingerprint": fp, "confirm": false })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("false"), "expected filed:false: {text}");
}
#[tokio::test]
async fn dispatch_report_bug_confirm_true_calls_backend() {
let fp = "c".repeat(64);
let resp = dispatch(
&MockBackend,
call("report_bug", json!({ "fingerprint": fp, "confirm": true })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("filed"), "expected 'filed' key: {text}");
}
#[tokio::test]
async fn dispatch_session_new_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_new",
json!({ "repo_url": "https://example.com/r.git", "ref": "main", "task": "do it" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("example.com")
);
}
#[tokio::test]
async fn dispatch_session_new_requires_repo_url() {
let resp = dispatch(
&MockBackend,
call("session_new", json!({ "ref": "main", "task": "x" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("repo_url")
);
}
#[tokio::test]
async fn dispatch_single_id_session_tools() {
for tool in [
"session_stop",
"session_resume",
"session_decommission",
"session_delete",
] {
let resp = dispatch(&MockBackend, call(tool, json!({ "session_id": "s9" }))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false, "{tool} should succeed");
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("s9"),
"{tool} should echo the id"
);
}
}
#[tokio::test]
async fn dispatch_session_stop_requires_id() {
let resp = dispatch(&MockBackend, call("session_stop", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("session_id")
);
}
#[tokio::test]
async fn dispatch_session_activity_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_activity",
json!({ "session_id": "s1", "lines": 25 }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("25"),
"should reflect the requested line count"
);
}
#[tokio::test]
async fn dispatch_session_activity_default_lines() {
let resp = dispatch(
&MockBackend,
call("session_activity", json!({ "session_id": "s1" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("60"),
"should default to 60 lines"
);
}
#[tokio::test]
async fn dispatch_session_send_tool() {
let resp = dispatch(
&MockBackend,
call("session_send", json!({ "session_id": "s1", "text": "yes" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("yes")
);
}
#[tokio::test]
async fn dispatch_session_send_requires_text() {
let resp = dispatch(
&MockBackend,
call("session_send", json!({ "session_id": "s1" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("text")
);
}
#[tokio::test]
async fn dispatch_session_context_catchup_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_context_catchup",
json!({ "project_dir": "/tmp/proj", "full": true }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("watermark_advanced")
);
}
#[tokio::test]
async fn dispatch_session_context_catchup_requires_project_dir() {
let resp = dispatch(&MockBackend, call("session_context_catchup", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("project_dir")
);
}
#[tokio::test]
async fn dispatch_session_context_pause_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_context_pause",
json!({
"project_dir": "/tmp/proj",
"session_id": "s1",
"summary": "Did the thing.",
"next_steps": ["ship it"],
"prune_worktrees": false,
}),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("snapshot_path"));
assert!(text.contains("ship it"));
assert!(text.contains("\"prune_worktrees\":false"));
}
#[tokio::test]
async fn dispatch_session_context_pause_requires_summary() {
let resp = dispatch(
&MockBackend,
call(
"session_context_pause",
json!({ "project_dir": "/tmp/proj", "session_id": "s1" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("summary")
);
}
#[tokio::test]
async fn dispatch_session_delete_defaults_force_false() {
let resp = dispatch(
&MockBackend,
call("session_delete", json!({ "session_id": "running-id" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true, "must refuse without --force");
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("running-id")
);
}
#[tokio::test]
async fn dispatch_session_delete_force_bypasses_guard() {
let resp = dispatch(
&MockBackend,
call(
"session_delete",
json!({ "session_id": "running-id", "force": true }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false, "force:true must bypass refusal");
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("\"deleted\":true")
);
}
#[tokio::test]
async fn dispatch_session_decommission_ephemeral_tool() {
let resp = dispatch(
&MockBackend,
call("session_decommission_ephemeral", json!({})),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("decommissioned")
);
}
#[tokio::test]
async fn dispatch_session_prune_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_prune",
json!({ "state": "ephemeral", "dry_run": true }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("ephemeral")
);
}
#[tokio::test]
async fn dispatch_session_prune_rejects_bad_state() {
let resp = dispatch(
&MockBackend,
call("session_prune", json!({ "state": "garbage" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("unknown prune filter")
);
}
#[tokio::test]
async fn dispatch_session_prune_requires_state() {
let resp = dispatch(&MockBackend, call("session_prune", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("state")
);
}
#[tokio::test]
async fn dispatch_project_list_tool() {
let resp = dispatch(&MockBackend, call("project_list", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("projects"), "expected 'projects' in: {text}");
}
#[tokio::test]
async fn dispatch_project_register_tool() {
let resp = dispatch(
&MockBackend,
call(
"project_register",
json!({
"name": "my-repo",
"repo_url": "https://github.com/o/my-repo",
"default_branch": "develop",
"stack_hint": "rust",
"tags": ["backend"],
"description": "a repo"
}),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("my-repo"), "expected 'my-repo' in: {text}");
}
#[tokio::test]
async fn dispatch_project_register_accepts_gh_user() {
let resp = dispatch(
&MockBackend,
call(
"project_register",
json!({
"name": "my-repo",
"repo_url": "https://github.com/o/my-repo",
"gh_user": "bobmatnyc"
}),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("bobmatnyc"),
"expected 'bobmatnyc' in: {text}"
);
}
#[tokio::test]
async fn dispatch_project_register_accepts_gh_account() {
let resp = dispatch(
&MockBackend,
call(
"project_register",
json!({
"name": "my-repo",
"repo_url": "https://github.com/o/my-repo",
"gh_account": "bobmatnyc"
}),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("bobmatnyc"),
"expected 'bobmatnyc' in: {text}"
);
}
#[tokio::test]
async fn dispatch_project_register_requires_name() {
let resp = dispatch(
&MockBackend,
call(
"project_register",
json!({ "repo_url": "https://github.com/o/repo" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("name")
);
}
#[tokio::test]
async fn dispatch_project_register_requires_repo_url() {
let resp = dispatch(
&MockBackend,
call("project_register", json!({ "name": "my-repo" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("repo_url")
);
}
#[tokio::test]
async fn dispatch_project_get_tool() {
let resp = dispatch(
&MockBackend,
call("project_get", json!({ "name": "trusty-tools" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("trusty-tools"),
"expected 'trusty-tools' in: {text}"
);
}
#[tokio::test]
async fn dispatch_project_get_missing_name() {
let resp = dispatch(
&MockBackend,
call("project_get", json!({ "name": "does-not-exist" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("not found")
);
}
#[tokio::test]
async fn dispatch_project_get_requires_name() {
let resp = dispatch(&MockBackend, call("project_get", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("name")
);
}
#[tokio::test]
async fn dispatch_project_resolve_tool() {
let resp = dispatch(
&MockBackend,
call("project_resolve", json!({ "query": "trusty-tools" })),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("trusty-tools"),
"expected 'trusty-tools' in: {text}"
);
assert!(
text.contains("confidence"),
"expected 'confidence' field in: {text}"
);
}
#[tokio::test]
async fn dispatch_project_resolve_no_match() {
let resp = dispatch(
&MockBackend,
call(
"project_resolve",
json!({ "query": "zzz-unregistered-xyz" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text)
.unwrap_or_else(|_| panic!("response text must be valid JSON, got: {text}"));
assert_eq!(
payload["primary"],
serde_json::Value::Null,
"primary must be null for a no-match response, got: {payload}"
);
let error_msg = payload["error"].as_str().unwrap_or_else(|| {
panic!("no-match response must carry an 'error' explanation, got: {payload}")
});
assert!(
error_msg.contains("zzz-unregistered-xyz"),
"error message must include the query, got: {error_msg}"
);
}
#[tokio::test]
async fn dispatch_project_resolve_requires_query() {
let resp = dispatch(&MockBackend, call("project_resolve", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("query")
);
}
#[tokio::test]
async fn dispatch_session_proxy_focus_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_proxy_focus",
json!({ "conversation_key": "conv-1", "session_id": "sess-abc" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("conv-1"), "{text}");
assert!(text.contains("sess-abc"), "{text}");
}
#[tokio::test]
async fn dispatch_session_proxy_focus_session_id_optional() {
let resp = dispatch(
&MockBackend,
call(
"session_proxy_focus",
json!({ "conversation_key": "conv-1" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false, "session_id must be optional");
}
#[tokio::test]
async fn dispatch_session_proxy_focus_requires_conversation_key() {
let resp = dispatch(&MockBackend, call("session_proxy_focus", json!({}))).await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("conversation_key")
);
}
#[tokio::test]
async fn dispatch_session_proxy_unfocus_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_proxy_unfocus",
json!({ "conversation_key": "conv-9" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("conv-9")
);
}
#[tokio::test]
async fn dispatch_session_proxy_message_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_proxy_message",
json!({ "conversation_key": "conv-2", "text": "run the tests" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("conv-2"), "{text}");
assert!(text.contains("run the tests"), "{text}");
}
#[tokio::test]
async fn dispatch_session_proxy_message_requires_text() {
let resp = dispatch(
&MockBackend,
call(
"session_proxy_message",
json!({ "conversation_key": "conv-2" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("text")
);
}
#[tokio::test]
async fn dispatch_session_proxy_summary_tool() {
let resp = dispatch(
&MockBackend,
call(
"session_proxy_summary",
json!({ "conversation_key": "conv-3" }),
),
)
.await;
let result = resp.result.unwrap();
assert_eq!(result["isError"], false);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("summary"), "{text}");
assert!(text.contains("conv-3"), "{text}");
}