use std::process::Command;
fn koda_bin() -> String {
let mut path = std::env::current_exe().unwrap();
path.pop(); path.pop(); path.push("koda");
path.to_string_lossy().to_string()
}
fn run_mock(prompt: &str, responses: &str) -> (String, String, bool) {
let tmp = tempfile::tempdir().unwrap();
let output = Command::new(koda_bin())
.args([
"-p",
prompt,
"--provider",
"mock",
"--output-format",
"json",
"--project-root",
])
.arg(tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.env("KODA_MOCK_RESPONSES", responses)
.output()
.expect("failed to run koda");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(stdout, stderr, output.status.success())
}
fn extract_json(stdout: &str) -> serde_json::Value {
let start = stdout
.find('{')
.unwrap_or_else(|| panic!("no JSON in stdout:\n{stdout}"));
serde_json::from_str(&stdout[start..])
.unwrap_or_else(|e| panic!("invalid JSON: {e}\nfrom: {}", &stdout[start..]))
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
chars.next(); for ch in chars.by_ref() {
if ch.is_ascii_alphabetic() {
break;
}
}
} else {
out.push(c);
}
}
out
}
#[test]
fn koda_docs_skill_appears_in_list_skills() {
let responses = r#"[
{"tool": "ListSkills", "args": {}},
{"text": "I can see the docs skill."}
]"#;
let (stdout, stderr, success) = run_mock("what skills are available?", responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("koda_docs"),
"expected 'koda_docs' in ListSkills output.\nstderr: {clean}"
);
}
#[test]
fn koda_docs_skill_activates_and_returns_manual_content() {
let responses = r#"[
{"tool": "ActivateSkill", "args": {"skill_name": "koda_docs"}},
{"text": "Done reading docs."}
]"#;
let (stdout, stderr, success) = run_mock("how do I use koda?", responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("koda_docs"),
"expected skill name in activation output.\nstderr: {clean}"
);
assert!(
clean.contains("https://lijunzh.github.io/koda/"),
"expected docs URL in tool result.\nstderr: {clean}"
);
}
#[test]
fn unknown_skill_returns_not_found_gracefully() {
let responses = r#"[
{"tool": "ActivateSkill", "args": {"skill_name": "no_such_skill_xyz"}},
{"text": "Skill was not available."}
]"#;
let (_stdout, stderr, success) = run_mock("activate a fake skill", responses);
assert!(
success,
"process crashed on missing skill.\nstderr: {stderr}"
);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("not found"),
"expected 'not found' message in tool result.\nstderr: {clean}"
);
}
fn invoke_agent_responses(agent_name: &str, prompt: &str) -> String {
serde_json::json!([
{"tool": "InvokeAgent", "args": {"agent_name": agent_name, "prompt": prompt}},
{"text": format!("{agent_name} delegation done")}
])
.to_string()
}
#[test]
fn explore_sub_agent_is_dispatched_when_invoked() {
let responses = invoke_agent_responses("explore", "find all Rust source files");
let (stdout, stderr, success) = run_mock("explore the codebase", &responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("explore"),
"expected SubAgentStart for 'explore' in stderr.\nstderr: {clean}"
);
assert!(
clean.contains("InvokeAgent"),
"expected InvokeAgent tool call in stderr.\nstderr: {clean}"
);
}
#[test]
fn plan_sub_agent_is_dispatched_when_invoked() {
let responses = invoke_agent_responses("plan", "design a caching layer");
let (stdout, stderr, success) = run_mock("plan a new feature", &responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("plan"),
"expected SubAgentStart for 'plan' in stderr.\nstderr: {clean}"
);
}
#[test]
fn verify_sub_agent_is_dispatched_when_invoked() {
let responses = invoke_agent_responses("verify", "check the auth module");
let (stdout, stderr, success) = run_mock("verify my changes", &responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("verify"),
"expected SubAgentStart for 'verify' in stderr.\nstderr: {clean}"
);
}
#[test]
fn task_sub_agent_is_dispatched_when_invoked() {
let responses = serde_json::json!([
{"tool": "InvokeAgent", "args": {"agent_name": "task", "prompt": "run a bash command"}},
{"text": "task completed"},
{"tool": "Bash", "args": {"command": "echo task_ran"}},
{"text": "task done"}
])
.to_string();
let (stdout, stderr, success) = run_mock("delegate a task", &responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
assert!(
clean.contains("task"),
"expected SubAgentStart for 'task' in stderr.\nstderr: {clean}"
);
}
#[test]
fn list_agents_shows_all_builtin_sub_agents() {
let responses = r#"[
{"tool": "ListAgents", "args": {}},
{"text": "I see the agents."}
]"#;
let (stdout, stderr, success) = run_mock("what agents are available?", responses);
assert!(success, "process failed.\nstderr: {stderr}");
let json = extract_json(&stdout);
assert_eq!(json["success"], true);
let clean = strip_ansi(&stderr);
for agent in ["explore", "plan", "verify", "task"] {
assert!(
clean.contains(agent),
"expected built-in agent '{agent}' in ListAgents output.\nstderr: {clean}"
);
}
assert!(
!clean.contains("guide"),
"'guide' agent should be gone — replaced by koda_docs skill.\nstderr: {clean}"
);
}