mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
a9121b3cf3
`craft-anything-cli capabilities` lists each tool with `available`, `needs_target` and `reason`. To show target tools as usable before anything is open, it marked every target tool available and dropped its reason. That also covered tools Clean room refuses, so in Clean room `native_function_pseudocode` read as available with no reason, though the engine refuses it. Only the "nothing is open yet" refusal is waived now. A target tool the mode refuses keeps `available: false` and its Clean room reason, with `needs_target: true` as before. In Deep mode nothing changes. **Verified:** a new CLI test runs `capabilities` in Clean room (the tool is unavailable with a reason) and with `--deep` (available, no reason). `cargo test -p craft-anything-cli` and clippy pass. Fixes #249
208 lines
11 KiB
Rust
208 lines
11 KiB
Rust
//! End-to-end tests of the built binary (run through the process runner).
|
|
|
|
use craft_anything_platform::process::{Output, Process};
|
|
use serde_json::{Value, json};
|
|
use std::time::Duration;
|
|
|
|
fn cli(args: &[&str], config: &std::path::Path) -> Output {
|
|
Process::new(env!("CARGO_BIN_EXE_craft-anything-cli")).args(args).env("CRAFT_ANYTHING_CONFIG_DIR", config).timeout(Duration::from_secs(60)).run().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn commands_and_exit_codes() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let out = cli(&["commands", "--format", "json"], cfg.path());
|
|
assert!(out.success);
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert!(v.as_array().unwrap().iter().any(|c| c["command"] == "core-session-show"));
|
|
assert_eq!(cli(&["no-such-tool"], cfg.path()).code, Some(2));
|
|
assert_eq!(cli(&["--bogus-flag"], cfg.path()).code, Some(2));
|
|
let fail = cli(&["core-finding-record", "--statement", "x", "--evidence-ids", "ev_nope"], cfg.path());
|
|
assert_eq!(fail.code, Some(1));
|
|
let e: Value = serde_json::from_slice(&fail.stdout).unwrap();
|
|
assert_eq!(e["error"]["code"], "bad_input");
|
|
}
|
|
|
|
/// #249: in Clean room a target tool the mode refuses was listed as available, because nothing
|
|
/// being open yet was waived for every target tool.
|
|
#[test]
|
|
fn capabilities_keep_the_clean_room_refusal() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let tool = |args: &[&str], id: &str| {
|
|
let out = cli(args, cfg.path());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
let tools = v["toolsets"].as_array().unwrap().iter().flat_map(|t| t["tools"].as_array().unwrap().clone());
|
|
tools.into_iter().find(|t| t["tool"] == id).unwrap_or_else(|| panic!("{id} not in {v}"))
|
|
};
|
|
let clean = tool(&["--format", "json", "capabilities"], "native_function_pseudocode");
|
|
assert_eq!((clean["available"].as_bool(), clean["needs_target"].as_bool()), (Some(false), Some(true)), "{clean}");
|
|
assert!(clean["reason"].as_str().is_some_and(|r| !r.is_empty()), "{clean}");
|
|
let deep = tool(&["--deep", "--format", "json", "capabilities"], "native_function_pseudocode");
|
|
assert_eq!((deep["available"].as_bool(), deep["reason"].is_null()), (Some(true), true), "{deep}");
|
|
}
|
|
|
|
/// After the tool name, `--project` is the tool's own option when it has one (it used to be taken
|
|
/// as the global investigation folder, which was then created there).
|
|
#[test]
|
|
fn a_tools_own_project_option_reaches_the_tool() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let folder = tempfile::tempdir().unwrap();
|
|
let out = cli(&["project-show", "--project", folder.path().to_str().unwrap()], cfg.path());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
// project_show read the folder (it isn't a project, so it says so) instead of missing its option.
|
|
let msg = v["error"]["message"].as_str().unwrap_or_default();
|
|
assert!(!msg.contains("missing field") && !msg.contains("`project`"), "{v}");
|
|
assert_eq!(std::fs::read_dir(folder.path()).unwrap().count(), 0, "no investigation was created in the folder");
|
|
}
|
|
|
|
#[test]
|
|
fn target_tools_open_then_close_and_project_journal() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("sample.bin");
|
|
std::fs::write(&file, b"\x7fELF not really").unwrap();
|
|
let proj = dir.path().join("p.cany");
|
|
let out = cli(&["--deep", "native-cursor-show", file.to_str().unwrap(), "--format", "json", "--project", proj.to_str().unwrap()], cfg.path());
|
|
assert!(out.success, "{}", out.stdout_text());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert_eq!(v["result"]["document"], "sample.bin");
|
|
let days: Vec<_> = std::fs::read_dir(proj.join("journal")).unwrap().collect();
|
|
assert_eq!(days.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn clean_room_refuses_to_look_inside_and_says_how_to_turn_on_deep_mode() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("sample.bin");
|
|
std::fs::write(&file, b"\x7fELF not really").unwrap();
|
|
let out = cli(&["core-target-open", file.to_str().unwrap()], cfg.path());
|
|
assert_eq!(out.code, Some(1), "{}", out.stdout_text());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert_eq!(v["error"]["details"]["reason"], "clean_room", "{v}");
|
|
assert!(v["error"]["remediation"].as_str().unwrap_or_default().contains("--deep"), "{v}");
|
|
// --deep is for this run only: nothing is saved.
|
|
assert!(cli(&["--deep", "core-target-open", file.to_str().unwrap()], cfg.path()).success);
|
|
assert!(!cfg.path().join("config.toml").exists(), "the run's mode isn't saved");
|
|
assert_eq!(cli(&["--deep", "--clean-room", "core-session-show"], cfg.path()).code, Some(2));
|
|
assert_eq!(cli(&["core-session-show", "--deep", "--clean-room"], cfg.path()).code, Some(2));
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_over_stdio() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let input = [
|
|
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "t", "version": "0"}}}),
|
|
json!({"jsonrpc": "2.0", "method": "notifications/initialized"}),
|
|
json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
|
|
json!({"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "core_session_show", "arguments": {}}}),
|
|
]
|
|
.iter()
|
|
.map(|v| format!("{v}\n"))
|
|
.collect::<String>();
|
|
let out = Process::new(env!("CARGO_BIN_EXE_craft-anything-cli"))
|
|
.arg("mcp")
|
|
.env("CRAFT_ANYTHING_CONFIG_DIR", cfg.path())
|
|
.stdin_bytes(input)
|
|
.timeout(Duration::from_secs(30))
|
|
.run()
|
|
.unwrap();
|
|
assert!(out.success);
|
|
let msgs: Vec<Value> = out.stdout_text().lines().map(|l| serde_json::from_str(l).unwrap()).collect();
|
|
let by_id = |id: u64| msgs.iter().find(|m| m["id"] == json!(id)).unwrap().clone();
|
|
assert_eq!(by_id(1)["result"]["serverInfo"]["name"], "craft-anything");
|
|
assert!(by_id(2)["result"]["tools"].as_array().unwrap().iter().any(|t| t["name"] == "core_session_show"));
|
|
assert_eq!(by_id(3)["result"]["isError"], false);
|
|
}
|
|
|
|
#[test]
|
|
fn raw_binary_open_params_tool_format_and_project_evidence_across_processes() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let raw = dir.path().join("raw.bin");
|
|
// push rbp; mov rbp, rsp; xor eax, eax; pop rbp; ret
|
|
std::fs::write(&raw, [0x55, 0x48, 0x89, 0xe5, 0x31, 0xc0, 0x5d, 0xc3]).unwrap();
|
|
let raw = raw.to_str().unwrap();
|
|
let proj = dir.path().join("p.cany");
|
|
let proj = proj.to_str().unwrap();
|
|
|
|
// A tool's own --format reaches the tool; the output format has --output-format.
|
|
let out = cli(
|
|
&["core-target-open", raw, "--format", "raw-binary", "--architecture", "x86_64", "--entry-point", "0x1000", "--output-format", "json", "--deep"],
|
|
cfg.path(),
|
|
);
|
|
assert!(out.success, "{}", out.stdout_text());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert_eq!(v["result"]["kind"], "raw-binary", "{v}");
|
|
|
|
// A target tool reopens the path with the raw-binary options.
|
|
let out = cli(
|
|
&["--deep", "native-function-pseudocode", raw, "--architecture", "x86_64", "--entry-point", "0x1000", "--function", "0x1000", "--project", proj],
|
|
cfg.path(),
|
|
);
|
|
assert!(out.success, "{}", out.stdout_text());
|
|
|
|
// A new process on the same project sees the stored evidence.
|
|
let out = cli(&["core-evidence-show", "--detail", "summary", "--project", proj], cfg.path());
|
|
assert!(out.success, "{}", out.stdout_text());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert!(v["result"]["total"].as_u64().unwrap() >= 1, "{v}");
|
|
let out = cli(&["core-session-show", "--project", proj], cfg.path());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert!(v["result"]["evidence_records"].as_u64().unwrap() >= 1, "{v}");
|
|
}
|
|
|
|
fn cli_with_stdin(args: &[&str], config: &std::path::Path, input: &[u8]) -> Output {
|
|
Process::new(env!("CARGO_BIN_EXE_craft-anything-cli"))
|
|
.args(args)
|
|
.env("CRAFT_ANYTHING_CONFIG_DIR", config)
|
|
.stdin_bytes(input.to_vec())
|
|
.timeout(Duration::from_secs(60))
|
|
.run()
|
|
.unwrap()
|
|
}
|
|
|
|
/// `--json -` takes a tool's parameters from a pipe, and `-` isn't read as the shorthand for JSON output.
|
|
#[test]
|
|
fn parameters_can_come_from_a_pipe() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("plain.txt");
|
|
std::fs::write(&file, b"nothing packed here").unwrap();
|
|
let params = json!({"path": file}).to_string();
|
|
let out = cli_with_stdin(&["core-packed-files-list", "--deep", "--json", "-"], cfg.path(), params.as_bytes());
|
|
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
// The file reached the tool: it says the file isn't packed, not that the path is missing.
|
|
assert_eq!(v["error"]["code"], "not_supported", "{v}");
|
|
let bad = cli_with_stdin(&["core-packed-files-list", "--deep", "--json", "-"], cfg.path(), b"not json");
|
|
assert_eq!(bad.code, Some(2), "{}", String::from_utf8_lossy(&bad.stderr));
|
|
let empty = cli_with_stdin(&["core-packed-files-list", "--deep", "--json", "-"], cfg.path(), b"");
|
|
assert_eq!(empty.code, Some(2), "an empty pipe is an error, not a wait");
|
|
}
|
|
|
|
#[test]
|
|
fn csv_and_tsv_output_show_a_tools_rows() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let out = cli(&["core-session-show", "--output-format", "csv"], cfg.path());
|
|
assert!(out.success);
|
|
let text = String::from_utf8_lossy(&out.stdout);
|
|
let mut lines = text.lines();
|
|
assert!(lines.next().unwrap().split(',').any(|c| c == "clean_room"), "{text}");
|
|
let tsv = cli(&["core-session-show", "--output-format", "tsv"], cfg.path());
|
|
assert!(String::from_utf8_lossy(&tsv.stdout).lines().next().unwrap().split('\t').any(|c| c == "clean_room"));
|
|
}
|
|
|
|
/// A bare argument is the file, not whichever string option sorts first (`contains`, `detail`).
|
|
#[test]
|
|
fn a_bare_argument_is_the_path_even_when_other_options_sort_first() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("plain.txt");
|
|
std::fs::write(&file, b"nothing packed here").unwrap();
|
|
let path = file.to_str().unwrap();
|
|
let packed: Value = serde_json::from_slice(&cli(&["core-packed-files-list", "--deep", path], cfg.path()).stdout).unwrap();
|
|
assert_eq!(packed["error"]["code"], "not_supported", "the file was read: {packed}");
|
|
let model: Value = serde_json::from_slice(&cli(&["formats-ai-weights-show", "--deep", path], cfg.path()).stdout).unwrap();
|
|
assert!(!model["error"]["message"].as_str().unwrap_or_default().contains("unknown variant"), "{model}");
|
|
}
|