use std::io::{BufRead, Write};
mod common;
use common::McpClient;
#[test]
fn initialize_list_and_unknown_method() {
let tmp = tempfile::tempdir().unwrap();
let mut c = McpClient::start_in(tmp.path());
let tools = c.request("tools/list", serde_json::json!({}));
let names: Vec<&str> = tools["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
assert_eq!(names.len(), 10, "all 10 contract tools served");
assert!(names.contains(&"jerrycan_design") && names.contains(&"jerrycan_check"));
for t in tools["tools"].as_array().unwrap() {
assert!(
t["outputSchema"].is_object(),
"tool {} must forward outputSchema: {t}",
t["name"]
);
}
let msg =
serde_json::json!({"jsonrpc": "2.0", "id": 99, "method": "bogus/method", "params": {}});
writeln!(c.stdin, "{msg}").unwrap();
let mut line = String::new();
c.stdout.read_line(&mut line).unwrap();
let v: serde_json::Value = serde_json::from_str(&line).unwrap();
assert_eq!(v["error"]["code"], -32601);
let pong = c.request("ping", serde_json::json!({}));
assert!(pong.as_object().unwrap().is_empty());
c.shutdown();
}
#[test]
fn docs_tools_work_through_mcp() {
let tmp = tempfile::tempdir().unwrap();
let mut c = McpClient::start_in(tmp.path());
let result = c.request(
"tools/call",
serde_json::json!({"name": "jerrycan_docs_search", "arguments": {"query": "override_dep"}}),
);
assert_eq!(result["isError"], false);
assert_eq!(result["structuredContent"]["results"][0]["page"], "testing");
let (err, payload) = c.call_tool(
"jerrycan_docs_search",
serde_json::json!({"query": "override_dep"}),
);
assert!(!err);
assert_eq!(payload["results"][0]["page"], "testing");
let (err, payload) = c.call_tool("jerrycan_docs_get", serde_json::json!({"page": "errors"}));
assert!(!err);
assert!(payload["markdown"].as_str().unwrap().contains("JC0404"));
c.shutdown();
}
#[test]
fn mcp_lists_ten_tools_including_schema() {
use jerrycan::platform::mcp::handle_message;
let r = handle_message(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#).unwrap();
assert!(r.contains("jerrycan_schema"), "{r}");
}
#[test]
fn oversized_line_gets_minus_32600_and_server_keeps_serving() {
let tmp = tempfile::tempdir().unwrap();
let mut c = McpClient::start_in(tmp.path());
let oversized = "x".repeat(17 * 1024 * 1024);
writeln!(c.stdin, "{oversized}").unwrap();
let mut line = String::new();
c.stdout.read_line(&mut line).unwrap();
let v: serde_json::Value = serde_json::from_str(&line).unwrap();
assert_eq!(v["error"]["code"], -32600, "{v}");
assert!(v["id"].is_null(), "oversized error carries a null id: {v}");
assert!(
v["error"]["message"].as_str().unwrap().contains("16 MiB"),
"{v}"
);
let pong = c.request("ping", serde_json::json!({}));
assert!(pong.as_object().unwrap().is_empty());
c.shutdown();
}
const GOLDEN: &str = include_str!("../../../conformance/designs/todo-api.design.json");
const REFERENCE: &str = include_str!("../../../conformance/designs/reference-slice.design.json");
#[test]
fn design_tool_questions_then_completes() {
let tmp = tempfile::tempdir().unwrap();
let mut c = McpClient::start_in(tmp.path());
let (err, payload) = c.call_tool(
"jerrycan_design",
serde_json::json!({"requirements": "todo backend"}),
);
assert!(!err);
assert_eq!(payload["status"], "questions");
assert!(
payload["questions"][0]["question"]
.as_str()
.unwrap()
.contains("draft")
);
let mut bad: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
bad["name"] = serde_json::json!("Todo API");
let (err, payload) = c.call_tool(
"jerrycan_design",
serde_json::json!({"requirements": "todo backend", "draft": bad}),
);
assert!(!err);
assert_eq!(payload["status"], "questions");
assert_eq!(payload["questions"][0]["id"], "/name");
let good: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
let (err, payload) = c.call_tool(
"jerrycan_design",
serde_json::json!({"requirements": "todo backend", "draft": good}),
);
assert!(!err);
assert_eq!(payload["status"], "complete");
let design_path = payload["design_path"].as_str().unwrap();
assert!(std::path::Path::new(design_path).exists());
assert!(payload["next_step"].as_str().unwrap().contains("scaffold"));
c.shutdown();
}
#[test]
fn scaffold_generate_and_list_through_mcp() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("design.json"), GOLDEN).unwrap();
let mut c = McpClient::start_in(tmp.path());
let app_dir = tmp.path().join("todo-api");
let (err, payload) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({
"design_path": tmp.path().join("design.json").to_str().unwrap(),
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err, "{payload}");
assert!(payload["created"].as_array().unwrap().len() > 10);
let (err, payload) = c.call_tool(
"jerrycan_generate",
serde_json::json!({
"kind": "route",
"path": "tags",
"directory": app_dir.to_str().unwrap(),
"design_slice": { "name": "tags", "endpoints": [
{ "operation_id": "list_tags", "method": "GET", "path": "/", "success": { "status": 200 } }
]},
}),
);
assert!(!err, "{payload}");
assert!(
payload["modified"]
.as_array()
.unwrap()
.iter()
.any(|p| p == "crates/app/src/main.rs")
);
assert!(app_dir.join("crates/routes/tags/src/lib.rs").exists());
let (err, payload) = c.call_tool(
"jerrycan_list_routes",
serde_json::json!({"directory": app_dir.to_str().unwrap()}),
);
assert!(!err);
assert!(
payload["routes"]
.as_array()
.unwrap()
.iter()
.any(|r| r["path"] == "/tags/")
);
c.shutdown();
}
#[test]
fn package_refuses_when_check_is_red() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("design.json"), GOLDEN).unwrap();
let mut c = McpClient::start_in(tmp.path());
let app_dir = tmp.path().join("todo-api");
let (err, _) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({
"design_path": tmp.path().join("design.json").to_str().unwrap(),
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err);
let (err, payload) = c.call_tool(
"jerrycan_package",
serde_json::json!({"target": "k8s", "directory": app_dir.to_str().unwrap()}),
);
assert!(err, "packaging a red-check app must error: {payload}");
assert!(
payload["error"].as_str().unwrap().contains("check"),
"error names the failed check gate: {payload}"
);
c.shutdown();
}
#[test]
fn partial_slice_replacement_warns_about_dropped_routes() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("design.json"), GOLDEN).unwrap();
let mut c = McpClient::start_in(tmp.path());
let app_dir = tmp.path().join("todo-api");
let (err, _) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({
"design_path": tmp.path().join("design.json").to_str().unwrap(),
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err);
let (err, payload) = c.call_tool(
"jerrycan_generate",
serde_json::json!({
"kind": "route",
"path": "todos",
"directory": app_dir.to_str().unwrap(),
"design_slice": { "name": "todos", "endpoints": [
{ "operation_id": "list_todos", "method": "GET", "path": "/", "success": { "status": 200 } }
]},
}),
);
assert!(!err, "{payload}");
let next = payload["next_step"].as_str().unwrap();
assert!(
next.contains("warning") && next.contains("route count"),
"{next}"
);
c.shutdown();
}
#[test]
fn generate_route_over_mcp_warns_about_dropped_agent_mod_lines() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("design.json"), GOLDEN).unwrap();
let mut c = McpClient::start_in(tmp.path());
let app_dir = tmp.path().join("todo-api");
let (err, _) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({
"design_path": tmp.path().join("design.json").to_str().unwrap(),
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err);
let lib = app_dir.join("crates/routes/todos/src/lib.rs");
let orig = std::fs::read_to_string(&lib).unwrap();
std::fs::write(&lib, format!("{orig}mod cross_sweep;\n")).unwrap();
let (err, payload) = c.call_tool(
"jerrycan_generate",
serde_json::json!({
"kind": "route",
"path": "todos",
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err, "{payload}");
let warnings = payload["warnings"]
.as_array()
.expect("dropped agent lines must surface in a `warnings` array");
let hit = warnings
.iter()
.find(|w| {
w["file"]
.as_str()
.is_some_and(|f| f.ends_with("todos/src/lib.rs"))
})
.expect("the dropped lib.rs must be named in warnings");
assert!(
hit["dropped_lines"]
.as_array()
.unwrap()
.iter()
.any(|l| l == "mod cross_sweep;"),
"the exact dropped line must be named: {hit}"
);
assert!(
payload["next_step"]
.as_str()
.unwrap()
.contains("cross_sweep"),
"next_step must also name the dropped wiring: {}",
payload["next_step"]
);
c.shutdown();
}
#[test]
fn slice_name_path_mismatch_gets_a_pointed_hint() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("design.json"), GOLDEN).unwrap();
let mut c = McpClient::start_in(tmp.path());
let app_dir = tmp.path().join("todo-api");
let (err, _) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({
"design_path": tmp.path().join("design.json").to_str().unwrap(),
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err);
let (err, payload) = c.call_tool(
"jerrycan_generate",
serde_json::json!({
"kind": "route",
"path": "widgets",
"directory": app_dir.to_str().unwrap(),
"design_slice": { "name": "gadgets", "endpoints": [
{ "operation_id": "list_gadgets", "method": "GET", "path": "/", "success": { "status": 200 } }
]},
}),
);
assert!(err);
let msg = payload["error"].as_str().unwrap();
assert!(
msg.contains("gadgets") && msg.contains("widgets"),
"must name both sides: {msg}"
);
c.shutdown();
}
#[test]
fn gen_tests_writes_tool_owned_acceptance_tests() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("design.json"), GOLDEN).unwrap();
let mut c = McpClient::start_in(tmp.path());
let app_dir = tmp.path().join("todo-api");
let (err, _) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({
"design_path": tmp.path().join("design.json").to_str().unwrap(),
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err);
let (err, payload) = c.call_tool(
"jerrycan_gen_tests",
serde_json::json!({
"module": "todos",
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(!err, "{payload}");
assert_eq!(
payload["tests_created"][0],
"crates/routes/todos/tests/acceptance.rs"
);
assert_eq!(payload["expected_failing"], 8, "6 success + 2 listed 404s");
assert!(payload["next_step"].as_str().unwrap().contains("implement"));
let file =
std::fs::read_to_string(app_dir.join("crates/routes/todos/tests/acceptance.rs")).unwrap();
assert!(file.contains("GENERATED by jerrycan gen-tests"));
let (err, payload) = c.call_tool(
"jerrycan_gen_tests",
serde_json::json!({
"module": "ghosts",
"directory": app_dir.to_str().unwrap(),
}),
);
assert!(err);
assert!(payload["error"].as_str().unwrap().contains("ghosts"));
c.shutdown();
}
#[test]
fn gen_tests_without_module_covers_all_modules_and_jobs_via_mcp() {
use jerrycan::platform::design::Design;
use jerrycan::platform::testgen::{AllAcceptance, write_all_acceptance};
let tmp = tempfile::tempdir().unwrap();
let app = tmp.path().join("app");
std::fs::create_dir_all(&app).unwrap();
std::fs::write(app.join("design.json"), REFERENCE).unwrap();
let mut c = McpClient::start_in(tmp.path());
let (err, payload) = c.call_tool(
"jerrycan_gen_tests",
serde_json::json!({ "directory": app.to_str().unwrap() }),
);
assert!(!err, "module-less gen_tests must succeed: {payload}");
let design = Design::from_path(&app.join("design.json")).unwrap();
let oracle = tmp.path().join("oracle");
std::fs::create_dir_all(&oracle).unwrap();
let AllAcceptance {
tests_created: expected_files,
expected_failing: expected_count,
..
} = write_all_acceptance(&oracle, &design).unwrap();
assert!(
expected_files.len() > 2 && expected_files.iter().any(|f| f.contains("jobs")),
"fixture must exercise many modules + jobs: {expected_files:?}"
);
let created: Vec<String> = payload["tests_created"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
assert_eq!(
created, expected_files,
"one suite per endpoint-bearing module, then jobs once"
);
for rel in &created {
let got = std::fs::read(app.join(rel)).unwrap_or_else(|e| panic!("{rel} not written: {e}"));
let want = std::fs::read(oracle.join(rel)).unwrap();
assert_eq!(
got, want,
"{rel} must match the per-module writer byte-for-byte"
);
}
assert_eq!(
payload["expected_failing"].as_u64().unwrap() as usize,
expected_count,
"aggregate = sum of module counts + jobs counted exactly once"
);
let (err, single) = c.call_tool(
"jerrycan_gen_tests",
serde_json::json!({ "module": "users", "directory": app.to_str().unwrap() }),
);
assert!(!err, "{single}");
assert_eq!(
single["tests_created"],
serde_json::json!([
"crates/routes/users/tests/acceptance.rs",
"crates/jobs/tests/acceptance.rs"
]),
"the single-module MCP contract is frozen"
);
assert_eq!(
std::fs::read(app.join("crates/routes/users/tests/acceptance.rs")).unwrap(),
std::fs::read(oracle.join("crates/routes/users/tests/acceptance.rs")).unwrap(),
"both MCP paths write the identical module suite"
);
c.shutdown();
}