use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::sync::Arc;
fn lex_bin_path() -> String {
let manifest = env!("CARGO_MANIFEST_DIR");
let workspace = std::path::Path::new(manifest)
.parent().unwrap() .parent().unwrap(); let target_dir = std::env::var("CARGO_TARGET_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| workspace.join("target"));
let me = std::env::current_exe().unwrap();
let profile = me
.ancestors()
.find_map(|p| {
let name = p.file_name()?.to_str()?;
if name == "deps" {
p.parent()
.and_then(|q| q.file_name())
.and_then(|s| s.to_str())
.map(String::from)
} else { None }
})
.unwrap_or_else(|| "debug".into());
target_dir.join(profile).join("lex").to_string_lossy().into_owned()
}
fn run_program(src: &str, fn_name: &str, args: Vec<Value>, policy: Policy) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(fn_name, args).unwrap_or_else(|e| panic!("call {fn_name}: {e}"))
}
#[test]
fn call_mcp_round_trips_lex_check_through_lex_serve_mcp() {
let lex_bin = lex_bin_path();
if !std::path::Path::new(&lex_bin).exists() {
eprintln!("skipping: lex binary not at {lex_bin}");
return;
}
let tmp = tempfile::tempdir().unwrap();
let server_cmd = format!(
"{lex_bin} serve --mcp --store {}",
tmp.path().display(),
);
let src = r#"
import "std.agent" as agent
import "std.str" as str
fn check_remotely(server :: Str, source :: Str) -> [mcp] Result[Str, Str] {
let body := str.concat("{\"source\":\"", str.concat(source, "\"}"))
agent.call_mcp(server, "lex_check", body)
}
"#;
let lex_src = r#"fn id(n :: Int) -> Int { n }"#;
let v = run_program(src, "check_remotely",
vec![Value::Str(server_cmd.into()), Value::Str(lex_src.to_string().into())],
Policy::permissive());
match &v {
Value::Variant { name, args } if name == "Ok" => match &args[0] {
Value::Str(s) => {
assert!(s.contains("\\\"ok\\\":true") || s.contains("\"ok\":true"),
"expected ok=true from lex_check, got: {s}");
assert!(s.contains("\"isError\":false"),
"expected isError=false, got: {s}");
}
other => panic!("expected Str, got {other:?}"),
},
Value::Variant { name, args } if name == "Err" => {
panic!("call_mcp returned Err: {:?}", args[0]);
}
other => panic!("unexpected result: {other:?}"),
}
}
#[test]
fn call_mcp_with_invalid_args_json_returns_err() {
let src = r#"
import "std.agent" as agent
fn bad_args(server :: Str) -> [mcp] Result[Str, Str] {
agent.call_mcp(server, "lex_check", "this is not json")
}
"#;
let v = run_program(src, "bad_args",
vec![Value::Str("nonexistent_command".into())],
Policy::permissive());
match &v {
Value::Variant { name, args } if name == "Err" => match &args[0] {
Value::Str(s) => assert!(s.contains("not valid JSON"),
"expected JSON-parse error, got: {s}"),
other => panic!("expected Str inside Err, got {other:?}"),
},
other => panic!("expected Err, got {other:?}"),
}
}