use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use serde_json::{Value, json};
fn session(dir: &Path, requests: &[Value]) -> Vec<Value> {
let mut child = Command::new(env!("CARGO_BIN_EXE_glacier"))
.arg("mcp")
.current_dir(dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("start glacier mcp");
let mut stdin = child.stdin.take().unwrap();
for request in requests {
writeln!(stdin, "{request}").unwrap();
}
drop(stdin);
let replies = BufReader::new(child.stdout.take().unwrap())
.lines()
.map(|line| serde_json::from_str(&line.unwrap()).unwrap())
.collect();
assert!(child.wait().unwrap().success());
replies
}
fn call(id: u64, tool: &str, arguments: Value) -> Value {
json!({"jsonrpc": "2.0", "id": id, "method": "tools/call",
"params": {"name": tool, "arguments": arguments}})
}
#[test]
fn an_agent_learns_the_language_and_checks_its_code() {
let dir = std::env::temp_dir().join(format!("glacier-mcp-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("src/models")).unwrap();
std::fs::create_dir_all(dir.join("src/controllers")).unwrap();
std::fs::write(
dir.join("src/models/post.glacier"),
"model Post do\n field title: String, required\n has_many :comments\nend\n",
)
.unwrap();
std::fs::write(
dir.join("src/controllers/posts_controller.glacier"),
"controller Posts do\n def show(ctx do\n end\nend\n",
)
.unwrap();
let replies = session(
&dir,
&[
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "test", "version": "0"}}}),
json!({"jsonrpc": "2.0", "method": "notifications/initialized"}),
json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
call(3, "get_syntax_reference", json!({})),
call(
4,
"search_docs",
json!({"query": "has_many through", "limit": 2}),
),
call(5, "list_models", json!({})),
call(6, "check", json!({"build": false})),
call(
7,
"show_generated_rust",
json!({"path": "src/models/post.glacier"}),
),
],
);
assert_eq!(
replies.len(),
7,
"every request answered, the notification not"
);
assert_eq!(replies[0]["result"]["serverInfo"]["name"], "glacier");
assert!(
replies[1]["result"]["tools"].as_array().unwrap().len() >= 10,
"{}",
replies[1]
);
assert!(
replies[2]["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("controller")
);
assert_eq!(
replies[3]["result"]["structuredContent"]["results"]
.as_array()
.unwrap()
.len(),
2
);
let models = &replies[4]["result"]["structuredContent"]["models"];
assert_eq!(models[0]["name"], "Post");
assert_eq!(models[0]["table"], "posts");
assert_eq!(models[0]["associations"][0]["name"], "comments");
let check = &replies[5]["result"]["structuredContent"];
assert_eq!(check["ok"], false);
let error = &check["diagnostics"][0];
assert_eq!(error["source"], "glacier");
assert_eq!(error["file"], "src/controllers/posts_controller.glacier");
assert_eq!(
(error["line"].clone(), error["column"].clone()),
(json!(2), json!(7))
);
let rust = replies[6]["result"]["content"][0]["text"].as_str().unwrap();
assert!(rust.contains("pub struct Post"), "{rust}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_agent_navigates_the_app() {
let dir = std::env::temp_dir().join(format!("glacier-mcp-nav-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("src/models")).unwrap();
std::fs::create_dir_all(dir.join("src/controllers")).unwrap();
std::fs::write(
dir.join("src/models/post.glacier"),
"model Post do\n field title: String, required\n belongs_to :author, User, touch\n\n before_save do\n self.title = self.title.trim().to_owned()\n end\nend\n",
)
.unwrap();
std::fs::write(
dir.join("src/controllers/posts_controller.glacier"),
"controller Posts do\n def show(ctx) do\n post = Post.find(ctx.id()?)!\n json post\n end\nend\n",
)
.unwrap();
let replies = session(
&dir,
&[
call(1, "list_symbols", json!({"kind": "action"})),
call(2, "describe_symbol", json!({"name": "Post"})),
call(3, "describe_callbacks", json!({"model": "Post"})),
call(4, "find_references", json!({"name": "Post"})),
call(5, "rename_symbol", json!({"from": "Post", "to": "Article"})),
call(6, "find_definition", json!({"name": "Posts#show"})),
],
);
let result = |i: usize| &replies[i]["result"]["structuredContent"];
assert_eq!(result(0)["symbols"][0]["name"], "show");
assert_eq!(result(1)["definitions"][0]["kind"], "model");
assert!(
result(1)["definitions"][0]["code"]
.as_str()
.unwrap()
.contains("before_save")
);
let steps: Vec<String> = result(2)["save"]
.as_array()
.unwrap()
.iter()
.map(|s| {
s["callback"]
.as_str()
.or(s["step"].as_str())
.unwrap()
.to_owned()
})
.collect();
assert_eq!(steps, ["validations", "before_save", "write", "touch"]);
assert_eq!(result(3)["references"].as_array().unwrap().len(), 2);
assert_eq!(result(4)["applied"], false);
assert_eq!(result(4)["changes"].as_array().unwrap().len(), 2);
let untouched = std::fs::read_to_string(dir.join("src/models/post.glacier")).unwrap();
assert!(
untouched.starts_with("model Post do"),
"a dry run changes nothing"
);
assert_eq!(result(5)["definitions"][0]["line"], 2);
std::fs::remove_dir_all(&dir).ok();
}