test: cost pipeline — SSH roundtrip + MCP cost dispatch + typo fix #243

Merged
clawdie merged 1 commit from test/cost-pipeline-tests into main 2026-06-27 22:12:12 +02:00
3 changed files with 150 additions and 2 deletions

View file

@ -0,0 +1,69 @@
use std::io::Write;
use std::process::{Command, Stdio};
/// Verify SSH to mother with report-task-cost forced command works.
/// Sends payload via stdin (matching daemon push_cost_to_mother behaviour).
#[test]
fn ssh_mother_report_task_cost_roundtrip() {
let mother = match std::env::var("COLIBRI_MOTHER_HOST") {
Ok(h) => h,
Err(_) => { eprintln!("SKIP: COLIBRI_MOTHER_HOST not set"); return; }
};
let payload = format!(r#"{{"node_hostname":"test-runner","task_id":"mcp-test-{}","provider":"deepseek","model":"deepseek-chat","input_tokens":10,"output_tokens":5,"cost_usd":0.0001,"success":true}}"#, std::process::id());
let mut child = Command::new("ssh")
.args(["-o","BatchMode=yes","-o","ConnectTimeout=5",&mother,"report-task-cost"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("ssh spawn failed");
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, "{payload}").unwrap();
}
let output = child.wait_with_output().expect("ssh wait failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("INSERT 0 1"),
"expected INSERT 0 1\ngot: {}\nstderr: {}",
stdout.trim(),
String::from_utf8_lossy(&output.stderr).trim());
}
/// Verify SSH forced command works for tools discovery.
#[test]
fn ssh_mother_tools_lists_daemon_tools() {
let mother = match std::env::var("COLIBRI_MOTHER_HOST") {
Ok(h) => h,
Err(_) => { eprintln!("SKIP: COLIBRI_MOTHER_HOST not set"); return; }
};
let output = Command::new("ssh")
.args(["-o","BatchMode=yes","-o","ConnectTimeout=5",&mother,"tools"])
.output()
.expect("ssh tools failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("colibri_list_tasks"), "tools missing list_tasks");
assert!(stdout.contains("colibri_external_mcp_servers"), "tools missing external_mcp_servers");
}
/// Verify unknown commands are rejected by the forced-command wrapper.
#[test]
fn ssh_mother_rejects_unknown_commands() {
let mother = match std::env::var("COLIBRI_MOTHER_HOST") {
Ok(h) => h,
Err(_) => { eprintln!("SKIP: COLIBRI_MOTHER_HOST not set"); return; }
};
let output = Command::new("ssh")
.args(["-o","BatchMode=yes","-o","ConnectTimeout=5",&mother,"bogus-command"])
.output()
.expect("ssh failed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success(), "bogus command should be rejected");
assert!(stderr.contains("rejected"), "should reject unknown commands, got: {}", stderr.trim());
}

View file

@ -209,7 +209,7 @@ pub fn tool_list() -> Vec<Value> {
),
// ── Deploy tools ──
json_tool(
"colibrie_deploy_run",
"colibri_deploy_run",
"Run a shell command on the host or in a Bastille jail. Use deploy_targets to list available targets.",
Some(serde_json::json!({
"type": "object",
@ -221,7 +221,7 @@ pub fn tool_list() -> Vec<Value> {
})),
),
json_tool(
"colibrie_deploy_targets",
"colibri_deploy_targets",
"List deploy targets: host and available Bastille jails.",
None,
),

View file

@ -255,5 +255,84 @@ fn tool_list_has_all_phase1_tools() {
assert!(names.contains(&"colibri_list_task_costs"));
assert!(names.contains(&"colibri_get_task"));
assert!(names.contains(&"colibri_deploy_run"));
assert!(names.contains(&"colibri_deploy_targets"));
assert!(names.contains(&"colibri_wiki_search"));
assert!(names.contains(&"colibri_wiki_page"));
assert!(names.contains(&"colibri_zfs_list_snapshots"));
assert!(names.contains(&"colibri_zfs_destroy_snapshot"));
assert!(names.contains(&"colibri_pf_list_rules"));
assert!(names.contains(&"colibri_pf_list_states"));
assert_eq!(names.len(), 20);
}
#[tokio::test]
async fn tool_list_task_costs_returns_cost_data() {
let socket = mock_daemon(json!({
"ok": true,
"data": [{
"id": "tc1",
"title": "cost test task",
"status": "Done",
"provider": "deepseek",
"model": "deepseek-chat",
"input_tokens": 45000,
"output_tokens": 2800,
"cache_read_tokens": 12000,
"cost_usd": 0.0042,
"success": true
}]
}))
.await;
let client = DaemonClient::new(&socket);
let cfg = config(socket, false);
let result = dispatch_tool(&client, &cfg, "colibri_list_task_costs", &json!({}))
.await
.unwrap();
let result_val: serde_json::Value = serde_json::to_value(&result).unwrap();
let text = result_val["content"][0]["text"].as_str().unwrap();
assert!(text.contains("tc1"));
assert!(text.contains("deepseek"));
assert!(text.contains("0.0042"));
}
#[tokio::test]
async fn tool_get_task_returns_cost_fields() {
let socket = mock_daemon(json!({
"ok": true,
"data": [{
"id": "tc99",
"title": "one task with cost",
"status": "Done",
"provider": "anthropic",
"model": "claude-sonnet-4",
"input_tokens": 1000,
"output_tokens": 500,
"cache_read_tokens": 300,
"cost_usd": 0.89,
"success": true
}]
}))
.await;
let client = DaemonClient::new(&socket);
let cfg = config(socket, false);
let result = dispatch_tool(
&client,
&cfg,
"colibri_get_task",
&json!({"task_id": "tc99"}),
)
.await
.unwrap();
let result_val: serde_json::Value = serde_json::to_value(&result).unwrap();
let text = result_val["content"][0]["text"].as_str().unwrap();
assert!(text.contains("tc99"));
assert!(text.contains("claude-sonnet-4"));
assert!(text.contains("0.89"));
}