test: cost pipeline — SSH roundtrip + MCP cost dispatch + typo fix #243
3 changed files with 150 additions and 2 deletions
69
crates/colibri-daemon/tests/cost_pipeline.rs
Normal file
69
crates/colibri-daemon/tests/cost_pipeline.rs
Normal 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());
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue