use cairn_core::{
node_at, render_addressed, Confidence, Editor, ExprSpec, ModuleSpec, NodeHash,
Store, Type, TypeDefSpec,
};
use serde_json::{json, Value};
use std::collections::BTreeSet;
pub struct Session {
editor: Editor,
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
impl Session {
pub fn new() -> Self {
let store = Store::open_in_memory().expect("in-memory store");
Self {
editor: Editor::new(store),
}
}
pub fn open(path: &str) -> Result<Self, String> {
let store = Store::open(path).map_err(|e| e.to_string())?;
Ok(Self {
editor: Editor::new(store),
})
}
pub fn handle(&mut self, req: Value) -> Value {
let id = req.get("id").cloned();
let method = req.get("method").and_then(Value::as_str).unwrap_or("");
let params = req.get("params").cloned().unwrap_or(Value::Null);
let outcome = match method {
"initialize" => Ok(json!({
"protocolVersion": "2024-11-05",
"serverInfo": { "name": "cairn-mcp-server", "version": env!("CARGO_PKG_VERSION") },
"capabilities": { "tools": {} }
})),
"tools/list" => Ok(json!({ "tools": tool_list() })),
"tools/call" => self.call(¶ms),
_ if id.is_none() => return Value::Null, _ => Err(Rpc::method_not_found(method)),
};
match (id, outcome) {
(None, _) => Value::Null,
(Some(id), Ok(result)) => json!({"jsonrpc":"2.0","id":id,"result":result}),
(Some(id), Err(e)) => {
json!({"jsonrpc":"2.0","id":id,"error":{"code":e.0,"message":e.1}})
}
}
}
fn call(&mut self, params: &Value) -> Result<Value, Rpc> {
let name = params
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| Rpc::invalid("missing tool name"))?;
let args = params.get("arguments").cloned().unwrap_or(json!({}));
match self.dispatch(name, &args) {
Ok(payload) => Ok(json!({
"content": [{ "type": "text", "text": payload.to_string() }],
"isError": false
})),
Err(msg) => Ok(json!({
"content": [{ "type": "text", "text": msg }],
"isError": true
})),
}
}
fn dispatch(&mut self, name: &str, a: &Value) -> Result<Value, String> {
let ok = || json!({ "ok": true });
match name {
"begin_edit" => self.editor.begin_edit().map(|_| ok()).map_err(es),
"abort_edit" => {
self.editor.abort_edit();
Ok(ok())
}
"create_function" => self
.editor
.create_function(str_arg(a, "name")?)
.map(|_| ok())
.map_err(es),
"add_param" => {
let ty: Type = de(a, "type")?;
let mc: Confidence = de(a, "min_confidence")?;
self.editor
.add_param(str_arg(a, "name")?, ty, mc)
.map(|_| ok())
.map_err(es)
}
"set_produces" => {
let ty: Type = de(a, "type")?;
let c: Confidence = de(a, "confidence")?;
self.editor.set_produces(ty, c).map(|_| ok()).map_err(es)
}
"set_effects" => {
let effects: BTreeSet<cairn_core::Effect> = de(a, "effects")?;
self.editor.set_effects(effects).map(|_| ok()).map_err(es)
}
"set_on_failure" => {
let f: Vec<String> = de(a, "failures")?;
self.editor.set_on_failure(f).map(|_| ok()).map_err(es)
}
"add_step" => {
let value: ExprSpec = de(a, "value")?;
self.editor
.add_step(str_arg(a, "binding")?, value)
.map(|_| ok())
.map_err(es)
}
"set_yield" => {
let value: ExprSpec = de(a, "value")?;
self.editor.set_yield(value).map(|_| ok()).map_err(es)
}
"describe_hole" => self
.editor
.describe_hole()
.map(|h| serde_json::to_value(h).unwrap())
.map_err(es),
"define_type" => {
let spec: TypeDefSpec = de(a, "type")?;
self.editor
.define_type(&spec)
.map(|h| json!({ "hash": h.to_string() }))
.map_err(es)
}
"apply_module" => {
let spec: ModuleSpec = de(a, "module")?;
self.editor
.apply_module(&spec)
.map(|(hash, report)| {
json!({ "hash": hash.to_string(), "report": report })
})
.map_err(es)
}
"commit_edit" => self
.editor
.commit_edit()
.map(|(hash, report)| {
json!({ "hash": hash.to_string(), "report": report })
})
.map_err(es),
"run" => {
let func = NodeHash::parse(str_arg(a, "func")?);
let fname = str_arg(a, "name")?;
let args: Vec<i64> = de(a, "args")?;
self.editor
.run(&func, fname, &args)
.map(|v| json!({ "result": v }))
.map_err(es)
}
"checkpoint" => {
let root = NodeHash::parse(str_arg(a, "root")?);
self.editor
.store()
.checkpoint(str_arg(a, "name")?, &root)
.map(|_| ok())
.map_err(es)
}
"branch" => {
let root = NodeHash::parse(str_arg(a, "root")?);
self.editor
.store()
.branch(str_arg(a, "name")?, &root)
.map(|_| ok())
.map_err(es)
}
"resolve" => self
.editor
.store()
.resolve(str_arg(a, "name")?)
.map(|o| json!({ "hash": o.map(|h| h.to_string()) }))
.map_err(es),
"query_type" => {
let m = NodeHash::parse(str_arg(a, "module")?);
self.editor
.query_type(&m, str_arg(a, "name")?)
.map(|o| serde_json::to_value(o).unwrap())
.map_err(es)
}
"render_addressed" => {
let root = NodeHash::parse(str_arg(a, "root")?);
render_addressed(self.editor.store(), &root)
.map(|ad| serde_json::to_value(ad).unwrap())
.map_err(es)
}
"node_at" => {
let root = NodeHash::parse(str_arg(a, "root")?);
let offset: usize = de(a, "offset")?;
let ad = render_addressed(self.editor.store(), &root)
.map_err(es)?;
Ok(json!({
"hash": node_at(&ad, offset).map(|h| h.to_string())
}))
}
"put_expr" => {
let spec: ExprSpec = de(a, "expr")?;
self.editor
.put_expr(&spec)
.map(|h| json!({ "hash": h.to_string() }))
.map_err(es)
}
"fill_hole" => {
let root = NodeHash::parse(str_arg(a, "root")?);
let hole = NodeHash::parse(str_arg(a, "hole")?);
let replacement = NodeHash::parse(str_arg(a, "replacement")?);
self.editor
.fill_hole(&root, &hole, &replacement)
.map(|(hash, report)| {
json!({ "root": hash.to_string(), "report": report })
})
.map_err(es)
}
"replace_node" => {
let root = NodeHash::parse(str_arg(a, "root")?);
let target = NodeHash::parse(str_arg(a, "target")?);
let replacement = NodeHash::parse(str_arg(a, "replacement")?);
self.editor
.replace_node(&root, &target, &replacement)
.map(|(hash, report)| {
json!({ "root": hash.to_string(), "report": report })
})
.map_err(es)
}
"find_references" => {
let root = NodeHash::parse(str_arg(a, "root")?);
let target = NodeHash::parse(str_arg(a, "target")?);
self.editor
.find_references(&root, &target)
.map(|refs| json!({ "references": refs }))
.map_err(es)
}
"run_module" => {
let module = NodeHash::parse(str_arg(a, "module")?);
let fname = str_arg(a, "name")?;
let args: Vec<i64> = de(a, "args")?;
self.editor
.run_module(&module, fname, &args)
.map(|v| json!({ "result": v }))
.map_err(es)
}
other => Err(format!("unknown tool: {other}")),
}
}
}
fn es<E: std::fmt::Display>(e: E) -> String {
e.to_string()
}
fn str_arg<'v>(a: &'v Value, key: &str) -> Result<&'v str, String> {
a.get(key)
.and_then(Value::as_str)
.ok_or_else(|| format!("missing or non-string argument `{key}`"))
}
fn de<T: serde::de::DeserializeOwned>(a: &Value, key: &str) -> Result<T, String> {
let v = a
.get(key)
.ok_or_else(|| format!("missing argument `{key}`"))?;
serde_json::from_value(v.clone()).map_err(|e| format!("bad argument `{key}`: {e}"))
}
struct Rpc(i64, String);
impl Rpc {
fn method_not_found(m: &str) -> Self {
Rpc(-32601, format!("method not found: {m}"))
}
fn invalid(m: &str) -> Self {
Rpc(-32602, m.to_string())
}
}
fn tool_list() -> Value {
let obj = || json!({ "type": "object" });
json!([
{ "name": "begin_edit", "description": "Open an edit transaction (non-nesting).", "inputSchema": obj() },
{ "name": "abort_edit", "description": "Discard the open draft.", "inputSchema": obj() },
{ "name": "create_function", "description": "Start a function draft.",
"inputSchema": { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] } },
{ "name": "add_param", "description": "Add a parameter (name, type, min_confidence).", "inputSchema": obj() },
{ "name": "set_produces", "description": "Set the produced type and confidence.", "inputSchema": obj() },
{ "name": "set_effects", "description": "Set the required effect set.", "inputSchema": obj() },
{ "name": "set_on_failure", "description": "Set declared failure variants.", "inputSchema": obj() },
{ "name": "add_step", "description": "Append a chain step (binding, value).", "inputSchema": obj() },
{ "name": "set_yield", "description": "Set the result expression.", "inputSchema": obj() },
{ "name": "describe_hole", "description": "What the body expects and what is in scope.", "inputSchema": obj() },
{ "name": "define_type", "description": "Define a record/variant type from a TypeDefSpec.", "inputSchema": obj() },
{ "name": "apply_module", "description": "Author a whole typed, multi-function module (ModuleSpec); returns the assembled-module check report.", "inputSchema": obj() },
{ "name": "commit_edit", "description": "Materialize, check, and return the report.", "inputSchema": obj() },
{ "name": "checkpoint", "description": "Name a root (immutable) so it survives the session.", "inputSchema": obj() },
{ "name": "branch", "description": "Name a root as a movable branch.", "inputSchema": obj() },
{ "name": "resolve", "description": "Resolve a checkpoint/branch name to its root hash.", "inputSchema": obj() },
{ "name": "query_type", "description": "The signature of a function in an applied module (params, produces, effects, failures, rendered).", "inputSchema": obj() },
{ "name": "find_references", "description": "Every node within a root's subtree that references a target node by hash (the basis for rename-as-label).", "inputSchema": obj() },
{ "name": "replace_node", "description": "Structurally replace every occurrence of a target node within a root; returns the new root hash and its check report.", "inputSchema": obj() },
{ "name": "fill_hole", "description": "Fill a typed hole with a node; accepted only if the result checks, else the hole remains and the report names the violated principle.", "inputSchema": obj() },
{ "name": "render_addressed", "description": "The Section-5 projection plus the hash<->span index (nested byte ranges) — the editor substrate.", "inputSchema": obj() },
{ "name": "node_at", "description": "The deepest node hash whose rendered span contains a byte offset (a cursor → the node to edit).", "inputSchema": obj() },
{ "name": "put_expr", "description": "Materialize an ExprSpec and return its node hash — the construction primitive feeding replace_node/fill_hole.", "inputSchema": obj() },
{ "name": "run", "description": "Lower a committed function and run it under wasmtime.", "inputSchema": obj() },
{ "name": "run_module", "description": "Lower an applied module and run one of its functions under wasmtime.", "inputSchema": obj() }
])
}
pub fn serve_stdio() {
use std::io::{BufRead, Write};
let mut session = match std::env::var("CAIRN_STORE") {
Ok(path) => Session::open(&path).unwrap_or_else(|e| {
eprintln!("cairn-mcp: cannot open store `{path}`: {e}");
std::process::exit(1);
}),
Err(_) => Session::new(),
};
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let req: Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
let _ = writeln!(
stdout,
"{}",
json!({"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":e.to_string()}})
);
continue;
}
};
let resp = session.handle(req);
if !resp.is_null() {
let _ = writeln!(stdout, "{resp}");
let _ = stdout.flush();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn call(s: &mut Session, id: i64, name: &str, args: Value) -> Value {
let resp = s.handle(json!({
"jsonrpc": "2.0", "id": id, "method": "tools/call",
"params": { "name": name, "arguments": args }
}));
let content = &resp["result"]["content"][0]["text"];
let text = content.as_str().expect("text content");
let is_error = resp["result"]["isError"].as_bool().unwrap_or(false);
if is_error {
json!({ "isError": true, "message": text })
} else {
serde_json::from_str(text).expect("tool result is JSON")
}
}
#[test]
fn initialize_and_tools_list() {
let mut s = Session::new();
let init = s.handle(json!({"jsonrpc":"2.0","id":1,"method":"initialize"}));
assert_eq!(init["result"]["serverInfo"]["name"], "cairn-mcp-server");
let list = s.handle(json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}));
let names: Vec<&str> = list["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
for expected in [
"create_function",
"commit_edit",
"run",
"query_type",
"find_references",
"replace_node",
"fill_hole",
"render_addressed",
"node_at",
"put_expr",
] {
assert!(names.contains(&expected), "missing tool {expected}");
}
}
#[test]
fn authors_checks_and_runs_a_function_over_jsonrpc() {
let mut s = Session::new();
assert_eq!(call(&mut s, 1, "begin_edit", json!({}))["ok"], true);
call(&mut s, 2, "create_function", json!({ "name": "id" }));
call(
&mut s,
3,
"add_param",
json!({ "name": "n", "type": "Number", "min_confidence": "External" }),
);
call(
&mut s,
4,
"set_produces",
json!({ "type": "Number", "confidence": "External" }),
);
call(&mut s, 5, "set_effects", json!({ "effects": [] }));
call(&mut s, 6, "set_yield", json!({ "value": { "Ref": "n" } }));
let committed = call(&mut s, 7, "commit_edit", json!({}));
assert_eq!(committed["report"]["status"], "Complete");
assert!(committed["report"]["violations"].as_array().unwrap().is_empty());
let hash = committed["hash"].as_str().unwrap().to_string();
let ran = call(
&mut s,
8,
"run",
json!({ "func": hash, "name": "id", "args": [7] }),
);
assert_eq!(ran["result"], 7);
}
#[test]
fn authors_a_v04_closure_function_over_mcp() {
let mut s = Session::new();
call(&mut s, 1, "begin_edit", json!({}));
call(&mut s, 2, "create_function", json!({ "name": "twice" }));
call(
&mut s,
3,
"add_param",
json!({ "name": "n", "type": "Number", "min_confidence": "External" }),
);
call(
&mut s,
4,
"set_produces",
json!({ "type": "Number", "confidence": "External" }),
);
call(&mut s, 5, "set_effects", json!({ "effects": [] }));
call(
&mut s,
6,
"set_yield",
json!({ "value": {
"CallValue": {
"callee": { "Lambda": {
"params": [["x", "Number"]],
"body": { "BinOp": {
"op": "Mul",
"lhs": { "Ref": "x" },
"rhs": { "Lit": 2 }
}}
}},
"args": [ { "Ref": "n" } ]
}
}}),
);
let committed = call(&mut s, 7, "commit_edit", json!({}));
assert_eq!(
committed["report"]["status"], "Complete",
"v0.4 closure must author cleanly over MCP: {committed:?}"
);
assert!(committed["report"]["violations"]
.as_array()
.unwrap()
.is_empty());
let hash = committed["hash"].as_str().unwrap().to_string();
let ran = call(
&mut s,
8,
"run",
json!({ "func": hash, "name": "twice", "args": [21] }),
);
assert_eq!(ran["result"], 42, "(|x| x*2)(21) over MCP");
}
#[test]
fn the_mcp_surface_authors_a_typed_multi_function_module() {
let mut s = Session::new();
let list = s.handle(json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}));
let names: Vec<String> = list["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap().to_string())
.collect();
for present in ["define_type", "apply_module"] {
assert!(
names.contains(&present.to_string()),
"module-level authoring tool `{present}` must be advertised"
);
}
let prod = json!({ "ty": "Number", "confidence": "External" });
let pnum = |n: &str| {
json!({ "name": n, "ty": "Number", "min_confidence": "External" })
};
let fnspec = |name: &str, params: Value, ty: Value, result: Value| {
json!({
"name": name, "type_params": [], "params": params,
"produces": ty, "requires": [], "on_failure": [],
"steps": [], "result": result
})
};
let module = json!({ "module": {
"name": "pairs",
"types": [ { "Record": {
"name": "Pair",
"fields": [ ["a", "Number"], ["b", "Number"] ]
}}],
"functions": [
fnspec("mk",
json!([ pnum("x") ]),
json!({ "ty": { "Named": "Pair" }, "confidence": "External" }),
json!({ "Record": { "type_name": "Pair", "fields": [
["a", { "Ref": "x" }],
["b", { "BinOp": { "op": "Mul",
"lhs": { "Ref": "x" }, "rhs": { "Lit": 2 } } }]
]}})),
fnspec("sum",
json!([ { "name": "p", "ty": { "Named": "Pair" },
"min_confidence": "External" } ]),
prod.clone(),
json!({ "BinOp": { "op": "Add",
"lhs": { "Field": { "base": { "Ref": "p" },
"type_name": "Pair", "field": "a" } },
"rhs": { "Field": { "base": { "Ref": "p" },
"type_name": "Pair", "field": "b" } } } })),
fnspec("run_it",
json!([ pnum("n") ]),
prod.clone(),
json!({ "Call": { "func": "sum", "args": [
{ "Call": { "func": "mk", "args": [ { "Ref": "n" } ] } }
]}})),
]
}});
let applied = call(&mut s, 2, "apply_module", module);
assert_eq!(
applied["report"]["status"], "Complete",
"typed multi-function module must verify over MCP: {applied:?}"
);
assert!(applied["report"]["violations"]
.as_array()
.unwrap()
.is_empty());
let hash = applied["hash"].as_str().unwrap().to_string();
let ran = call(
&mut s,
3,
"run_module",
json!({ "module": hash, "name": "run_it", "args": [5] }),
);
assert_eq!(ran["result"], 15, "sum(mk(5)) over MCP: ran={ran:?}");
}
#[test]
fn a_project_persists_across_sessions() {
let mut path = std::env::temp_dir();
path.push(format!("cairn-proj-{}.sqlite", std::process::id()));
let _ = std::fs::remove_file(&path);
let p = path.to_str().unwrap().to_string();
let hash = {
let mut s = Session::open(&p).expect("open file-backed session");
call(&mut s, 1, "begin_edit", json!({}));
call(&mut s, 2, "create_function", json!({ "name": "answer" }));
call(
&mut s,
3,
"set_produces",
json!({ "type": "Number", "confidence": "External" }),
);
call(&mut s, 4, "set_effects", json!({ "effects": [] }));
call(&mut s, 5, "set_yield", json!({ "value": { "Lit": 42 } }));
let committed = call(&mut s, 6, "commit_edit", json!({}));
let hash = committed["hash"].as_str().unwrap().to_string();
let cp = call(
&mut s,
7,
"checkpoint",
json!({ "name": "release", "root": hash }),
);
assert_eq!(cp["ok"], true);
hash
};
let mut s2 = Session::open(&p).expect("reopen the same store");
let resolved = call(&mut s2, 8, "resolve", json!({ "name": "release" }));
assert_eq!(
resolved["hash"].as_str().unwrap(),
hash,
"checkpoint must resolve to the same root in a new session"
);
let ran = call(
&mut s2,
9,
"run",
json!({ "func": hash, "name": "answer", "args": [] }),
);
assert_eq!(ran["result"], 42, "the persisted function still runs");
std::fs::remove_file(&path).unwrap();
}
#[test]
fn query_type_reports_a_signature_over_mcp() {
let mut s = Session::new();
let module = json!({ "module": {
"name": "m",
"types": [],
"functions": [{
"name": "add", "type_params": [],
"params": [
{ "name": "a", "ty": "Number", "min_confidence": "External" },
{ "name": "b", "ty": "Number", "min_confidence": "External" }
],
"produces": { "ty": "Number", "confidence": "External" },
"requires": [], "on_failure": [], "steps": [],
"result": { "BinOp": { "op": "Add",
"lhs": { "Ref": "a" }, "rhs": { "Ref": "b" } } }
}]
}});
let applied = call(&mut s, 1, "apply_module", module);
assert_eq!(applied["report"]["status"], "Complete");
let h = applied["hash"].as_str().unwrap().to_string();
let sig = call(
&mut s,
2,
"query_type",
json!({ "module": h, "name": "add" }),
);
assert_eq!(sig["name"], "add");
assert_eq!(sig["params"].as_array().unwrap().len(), 2);
assert_eq!(sig["params"][0]["name"], "a");
assert_eq!(sig["params"][0]["ty"], "Number");
assert_eq!(sig["produces"]["ty"], "Number");
assert!(
sig["rendered"].as_str().unwrap().contains("function add"),
"rendered projection for review: {}",
sig["rendered"]
);
let none = call(
&mut s,
3,
"query_type",
json!({ "module": h, "name": "nope" }),
);
assert!(none.is_null(), "missing function → null: {none:?}");
}
#[test]
fn find_references_round_trips_over_mcp() {
let mut s = Session::new();
let module = json!({ "module": {
"name": "m",
"types": [],
"functions": [{
"name": "add", "type_params": [],
"params": [
{ "name": "a", "ty": "Number", "min_confidence": "External" },
{ "name": "b", "ty": "Number", "min_confidence": "External" }
],
"produces": { "ty": "Number", "confidence": "External" },
"requires": [], "on_failure": [], "steps": [],
"result": { "BinOp": { "op": "Add",
"lhs": { "Ref": "a" }, "rhs": { "Ref": "b" } } }
}]
}});
let applied = call(&mut s, 1, "apply_module", module);
let h = applied["hash"].as_str().unwrap().to_string();
let r = call(
&mut s,
2,
"find_references",
json!({ "root": h, "target": h }),
);
assert_eq!(r["references"].as_array().unwrap().len(), 0);
}
#[test]
fn replace_node_round_trips_over_mcp() {
let mut s = Session::new();
let module = json!({ "module": {
"name": "m",
"types": [],
"functions": [{
"name": "k", "type_params": [],
"params": [],
"produces": { "ty": "Number", "confidence": "External" },
"requires": [], "on_failure": [], "steps": [],
"result": { "Lit": 1 }
}]
}});
let applied = call(&mut s, 1, "apply_module", module);
let h = applied["hash"].as_str().unwrap().to_string();
let r = call(
&mut s,
2,
"replace_node",
json!({ "root": h, "target": h, "replacement": h }),
);
assert_eq!(r["root"], h);
assert_eq!(r["report"]["status"], "Complete");
}
#[test]
fn the_editor_substrate_round_trips_over_mcp() {
let mut s = Session::new();
let nf = |name: &str, lit: i64| {
json!({
"name": name, "type_params": [], "params": [],
"produces": { "ty": "Number", "confidence": "External" },
"requires": [], "on_failure": [], "steps": [],
"result": { "Lit": lit }
})
};
let applied = call(
&mut s,
1,
"apply_module",
json!({ "module": {
"name": "m", "types": [],
"functions": [nf("k", 1), nf("j", 2)]
}}),
);
let root = applied["hash"].as_str().unwrap().to_string();
let a = call(&mut s, 2, "render_addressed", json!({ "root": root }));
let text = a["text"].as_str().unwrap().to_string();
assert!(!text.contains('\u{1}'), "no sentinel leaks: {text:?}");
assert!(text.contains("yield 1") && text.contains("yield 2"));
let off1 = text.find("yield 1").unwrap() + "yield ".len();
let off2 = text.find("yield 2").unwrap() + "yield ".len();
let at = |s: &mut Session, id, off: usize| {
call(s, id, "node_at", json!({ "root": root, "offset": off }))
["hash"]
.as_str()
.unwrap()
.to_string()
};
let lit1 = at(&mut s, 3, off1);
let lit2 = at(&mut s, 4, off2);
assert_ne!(lit1, lit2);
let rep = call(
&mut s,
5,
"replace_node",
json!({ "root": root, "target": lit1, "replacement": lit2 }),
);
assert_eq!(rep["report"]["status"], "Complete");
let root2 = rep["root"].as_str().unwrap().to_string();
let a2 = call(&mut s, 6, "render_addressed", json!({ "root": root2 }));
let text2 = a2["text"].as_str().unwrap();
assert!(
!text2.contains("yield 1") && text2.matches("yield 2").count() == 2,
"both functions now yield 2: {text2:?}"
);
let none = call(
&mut s,
7,
"node_at",
json!({ "root": root, "offset": 100_000 }),
);
assert!(none["hash"].is_null());
}
#[test]
fn fill_hole_rejects_a_non_hole_over_mcp() {
let mut s = Session::new();
let module = json!({ "module": {
"name": "m", "types": [],
"functions": [{
"name": "k", "type_params": [], "params": [],
"produces": { "ty": "Number", "confidence": "External" },
"requires": [], "on_failure": [], "steps": [],
"result": { "Lit": 1 }
}]
}});
let h = call(&mut s, 1, "apply_module", module)["hash"]
.as_str()
.unwrap()
.to_string();
let r = call(
&mut s,
2,
"fill_hole",
json!({ "root": h, "hole": h, "replacement": h }),
);
assert_eq!(r["isError"], true);
assert!(
r["message"].as_str().unwrap().contains("not a hole"),
"message: {}",
r["message"]
);
}
#[test]
fn a_tool_error_is_reported_not_panicked() {
let mut s = Session::new();
let r = call(&mut s, 1, "create_function", json!({ "name": "x" }));
assert_eq!(r["isError"], true);
}
#[test]
fn unknown_method_is_a_jsonrpc_error() {
let mut s = Session::new();
let r = s.handle(json!({"jsonrpc":"2.0","id":9,"method":"no/such"}));
assert_eq!(r["error"]["code"], -32601);
}
}