use std::collections::HashMap;
use interpretthis::{
Interpreter, InterpreterConfig, InterpreterDeps, InterpreterResponse, KwargsExt,
ToolDefinition, Tools, Value, ValueKey, shared_list,
};
use crate::common::{assert_error, assert_output};
fn interpreter() -> Interpreter {
Interpreter::new(InterpreterDeps { tools: Tools::new() }, InterpreterConfig::default())
}
fn no_tools() -> Tools {
Tools::new()
}
fn err_msg(resp: &InterpreterResponse) -> String {
resp.error.as_ref().map(ToString::to_string).unwrap_or_default()
}
#[tokio::test]
async fn engine_execute_simple_assignment() {
let interp = interpreter();
let resp = interp.execute("x = 42", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
}
#[tokio::test]
async fn engine_print_writes_to_stdout() {
let interp = interpreter();
let resp = interp.execute("print('hello world')", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
assert!(resp.stdout.contains("hello world"));
}
#[tokio::test]
async fn engine_default_config_executes() {
let interp = interpreter();
let resp = interp.execute("print('works')", &no_tools(), HashMap::new()).await;
assert!(resp.is_ok());
assert_eq!(resp.stdout.trim(), "works");
}
#[tokio::test]
async fn engine_response_is_ok_on_success() {
let interp = interpreter();
let resp = interp.execute("print('done')", &no_tools(), HashMap::new()).await;
assert!(resp.is_ok());
assert!(resp.result().is_ok());
}
#[tokio::test]
async fn engine_response_is_err_on_raise() {
let interp = interpreter();
let resp = interp.execute("raise ValueError('oops')", &no_tools(), HashMap::new()).await;
assert!(!resp.is_ok());
assert!(resp.result().is_err());
}
#[tokio::test]
async fn engine_state_persists_across_execute_calls() {
let interp = interpreter();
let resp1 = interp.execute("x = 42", &no_tools(), HashMap::new()).await;
assert!(resp1.error.is_none(), "error: {:?}", resp1.error);
let resp2 = interp.execute("print(x)", &no_tools(), HashMap::new()).await;
assert!(resp2.error.is_none(), "error: {:?}", resp2.error);
assert_eq!(resp2.stdout.trim(), "42");
}
#[tokio::test]
async fn engine_state_get_variable() {
let interp = interpreter();
let resp = interp.execute("x = 42\ny = 'hello'", &no_tools(), HashMap::new()).await;
assert!(resp.is_ok());
assert_eq!(interp.get_variable("x"), Some(Value::Int(42)));
assert_eq!(interp.get_variable("y"), Some(Value::String("hello".into())));
assert_eq!(interp.get_variable("nonexistent"), None);
}
#[tokio::test]
async fn engine_variables_map_accepts_json_value() {
let interp = interpreter();
let config = Value::from_json(serde_json::json!({
"name": "test",
"count": 5,
"items": [1, 2, 3]
}));
let variables = HashMap::from([("config".to_string(), config)]);
let resp = interp
.execute(
r#"
name = config["name"]
count = config["count"]
items = config["items"]
print(f"{name},{count},{len(items)}")
"#,
&no_tools(),
variables,
)
.await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "test,5,3");
}
#[tokio::test]
async fn engine_variables_map_accepts_from_impls() {
let interp = interpreter();
let variables = HashMap::from([
("name".to_string(), "Alice".into()),
("age".to_string(), 30i64.into()),
("active".to_string(), true.into()),
]);
let resp = interp.execute("print(f'{name},{age},{active}')", &no_tools(), variables).await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "Alice,30,True");
}
#[tokio::test]
async fn engine_registered_tool_via_deps() {
let tools = Tools::new().with(
"double",
ToolDefinition::from_fn(|kwargs| async move {
let n = kwargs.get_int("n").unwrap_or(0);
Ok(Value::Int(n * 2))
}),
);
let interp = Interpreter::new(InterpreterDeps { tools }, InterpreterConfig::default());
let resp =
interp.execute("result = double(n=21)\nprint(result)", &Tools::new(), HashMap::new()).await;
assert!(resp.is_ok(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "42");
}
#[tokio::test]
async fn engine_registered_tools_merge_with_execute_tools() {
let registered = Tools::new().with(
"registered",
ToolDefinition::from_fn(|_kwargs| async move { Ok(Value::String("from_builder".into())) }),
);
let interp =
Interpreter::new(InterpreterDeps { tools: registered }, InterpreterConfig::default());
let extra = Tools::new().with(
"extra",
ToolDefinition::from_fn(|_kwargs| async move { Ok(Value::String("from_execute".into())) }),
);
let resp = interp
.execute("a = registered()\nb = extra()\nprint(f'{a},{b}')", &extra, HashMap::new())
.await;
assert!(resp.is_ok(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "from_builder,from_execute");
}
#[tokio::test]
async fn engine_delete_builtin_name_rejected() {
let interp = interpreter();
let resp = interp.execute("del print", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "deleting a protected builtin should error");
}
#[tokio::test]
async fn engine_return_at_top_level_does_not_panic() {
let interp = interpreter();
let resp = interp.execute("x = 1\nreturn x", &no_tools(), HashMap::new()).await;
let _ = resp;
}
#[test]
fn engine_value_from_bool() {
let v: Value = true.into();
assert!(matches!(v, Value::Bool(true)));
}
#[test]
fn engine_value_from_i64() {
let v: Value = 42i64.into();
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn engine_value_from_i32() {
let v: Value = 7i32.into();
assert!(matches!(v, Value::Int(7)));
}
#[test]
fn engine_value_from_f64() {
let v: Value = 2.5f64.into();
match v {
Value::Float(f) => assert!((f - 2.5).abs() < f64::EPSILON),
other => panic!("expected Float, got {other:?}"),
}
}
#[test]
fn engine_value_from_string() {
let v: Value = String::from("hello").into();
assert!(matches!(v, Value::String(s) if s == "hello"));
}
#[test]
fn engine_value_from_str() {
let v: Value = "world".into();
assert!(matches!(v, Value::String(s) if s == "world"));
}
#[test]
fn engine_value_from_vec() {
let v: Value = vec![Value::Int(1), Value::Int(2)].into();
match v {
Value::List(items) => assert_eq!(items.lock().len(), 2),
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn engine_value_from_option_some() {
let v: Value = Some(42i64).into();
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn engine_value_from_option_none() {
let v: Value = Option::<i64>::None.into();
assert!(matches!(v, Value::None));
}
#[test]
fn engine_value_from_json_null() {
let v = Value::from_json(serde_json::Value::Null);
assert!(matches!(v, Value::None));
}
#[test]
fn engine_value_from_json_bool() {
let v = Value::from_json(serde_json::json!(true));
assert!(matches!(v, Value::Bool(true)));
}
#[test]
fn engine_value_from_json_integer() {
let v = Value::from_json(serde_json::json!(42));
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn engine_value_from_json_float() {
let v = Value::from_json(serde_json::json!(2.5));
match v {
Value::Float(f) => assert!((f - 2.5).abs() < f64::EPSILON),
other => panic!("expected Float, got {other:?}"),
}
}
#[test]
fn engine_value_from_json_string() {
let v = Value::from_json(serde_json::json!("hello"));
assert!(matches!(v, Value::String(s) if s == "hello"));
}
#[test]
fn engine_value_from_json_array() {
let v = Value::from_json(serde_json::json!([1, "two", null]));
match v {
Value::List(items) => {
let snapshot = items.lock().clone();
assert_eq!(snapshot.len(), 3);
assert!(matches!(snapshot[0], Value::Int(1)));
assert!(matches!(&snapshot[1], Value::String(s) if s == "two"));
assert!(matches!(snapshot[2], Value::None));
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn engine_value_from_json_object() {
let v = Value::from_json(serde_json::json!({"name": "Alice", "age": 30}));
match v {
Value::Dict(map) => {
assert_eq!(map.len(), 2);
assert!(
matches!(map.get(&ValueKey::String("name".into())), Some(Value::String(s)) if s == "Alice")
);
assert!(matches!(map.get(&ValueKey::String("age".into())), Some(Value::Int(30))));
}
other => panic!("expected Dict, got {other:?}"),
}
}
#[test]
fn engine_value_from_json_nested() {
let v = Value::from_json(serde_json::json!({
"users": [{"name": "Bob", "active": true}],
"count": 1
}));
match v {
Value::Dict(map) => {
assert_eq!(map.len(), 2);
match map.get(&ValueKey::String("users".into())) {
Some(Value::List(users)) => {
let snapshot = users.lock().clone();
assert_eq!(snapshot.len(), 1);
assert!(matches!(&snapshot[0], Value::Dict(_)));
}
other => panic!("expected List for 'users', got {other:?}"),
}
}
other => panic!("expected Dict, got {other:?}"),
}
}
#[test]
fn engine_value_to_json_primitives() {
assert_eq!(Value::None.to_json(), serde_json::Value::Null);
assert_eq!(Value::Bool(true).to_json(), serde_json::json!(true));
assert_eq!(Value::Int(42).to_json(), serde_json::json!(42));
assert_eq!(Value::String("hi".into()).to_json(), serde_json::json!("hi"));
}
#[test]
fn engine_value_to_json_collections() {
let list = Value::List(shared_list(vec![Value::Int(1), Value::String("two".into())]));
assert_eq!(list.to_json(), serde_json::json!([1, "two"]));
let mut map = indexmap::IndexMap::new();
map.insert(ValueKey::String("a".into()), Value::Int(1));
let dict = Value::Dict(map);
assert_eq!(dict.to_json(), serde_json::json!({"a": 1}));
}
#[test]
fn engine_value_json_round_trip() {
let original = serde_json::json!({
"name": "test",
"values": [1, 2.5, true, null, "hello"],
"nested": {"inner": [3, 4]}
});
let value = Value::from_json(original.clone());
let back = value.to_json();
assert_eq!(original, back);
}
#[test]
fn engine_value_as_str() {
assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
assert_eq!(Value::Int(42).as_str(), None);
assert_eq!(Value::None.as_str(), None);
}
#[test]
fn engine_value_as_int() {
assert_eq!(Value::Int(42).as_int(), Some(42));
assert_eq!(Value::String("hi".into()).as_int(), None);
assert_eq!(Value::Bool(true).as_int(), None);
}
#[test]
fn engine_value_as_float() {
assert_eq!(Value::Float(2.5).as_float(), Some(2.5));
assert_eq!(Value::Int(5).as_float(), Some(5.0));
assert_eq!(Value::String("hi".into()).as_float(), None);
}
#[test]
fn engine_value_as_bool() {
assert_eq!(Value::Bool(true).as_bool(), Some(true));
assert_eq!(Value::Bool(false).as_bool(), Some(false));
assert_eq!(Value::Int(1).as_bool(), None);
}
#[test]
fn engine_value_as_list() {
let list = Value::List(shared_list(vec![Value::Int(1), Value::Int(2)]));
assert_eq!(list.as_list().unwrap().len(), 2);
assert!(Value::Int(1).as_list().is_none());
}
#[test]
fn engine_value_as_dict() {
let mut map = indexmap::IndexMap::new();
map.insert(ValueKey::String("a".into()), Value::Int(1));
let dict = Value::Dict(map);
assert_eq!(dict.as_dict().unwrap().len(), 1);
assert!(Value::Int(1).as_dict().is_none());
}
#[test]
fn engine_value_try_into_string() {
let v = Value::String("hello".into());
assert_eq!(v.try_into_string().unwrap(), "hello");
let v = Value::Int(42);
assert!(v.try_into_string().is_err());
}
#[test]
fn engine_value_try_into_list() {
let v = Value::List(shared_list(vec![Value::Int(1)]));
assert_eq!(v.try_into_list().unwrap().len(), 1);
let v = Value::String("hi".into());
assert!(v.try_into_list().is_err());
}
#[test]
fn engine_value_partial_eq() {
assert_eq!(Value::None, Value::None);
assert_eq!(Value::Bool(true), Value::Bool(true));
assert_ne!(Value::Bool(true), Value::Bool(false));
assert_eq!(Value::Int(42), Value::Int(42));
assert_ne!(Value::Int(1), Value::Int(2));
assert_eq!(Value::Float(2.5), Value::Float(2.5));
assert_eq!(Value::String("hello".into()), Value::String("hello".into()));
assert_ne!(Value::String("a".into()), Value::String("b".into()));
assert_eq!(
Value::List(shared_list(vec![Value::Int(1), Value::Int(2)])),
Value::List(shared_list(vec![Value::Int(1), Value::Int(2)]))
);
assert_ne!(Value::Int(42), Value::String("42".into()));
}
#[tokio::test]
async fn collections_requires_import() {
assert_error("print(collections.Counter('abc'))").await;
}
#[tokio::test]
async fn bare_math_namespace_is_name_error() {
let interp = interpreter();
let resp = interp.execute("print(math.sqrt(9))", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected NameError, got stdout: {:?}", resp.stdout);
assert!(err_msg(&resp).contains("math"), "error should name 'math': {}", err_msg(&resp));
}
#[tokio::test]
async fn bare_collections_namespace_is_name_error() {
let interp = interpreter();
let resp =
interp.execute("print(collections.Counter(\"aabbc\"))", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected NameError, got stdout: {:?}", resp.stdout);
assert!(
err_msg(&resp).contains("collections"),
"error should name 'collections': {}",
err_msg(&resp)
);
}
#[tokio::test]
async fn bare_json_namespace_auto_imported() {
let interp = interpreter();
let resp = interp.execute("print(json.dumps({\"a\": 1}))", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "{\"a\": 1}");
}
#[tokio::test]
async fn bare_re_namespace_auto_imported() {
let interp = interpreter();
let resp =
interp.execute(r#"print(re.findall(r"\d+", "a1b22"))"#, &no_tools(), HashMap::new()).await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "['1', '22']");
}
#[tokio::test]
async fn bare_datetime_namespace_auto_imported() {
let interp = interpreter();
let resp =
interp.execute("print(datetime.date(2026, 1, 1))", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_none(), "error: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "2026-01-01");
}
#[tokio::test]
async fn eval_call_blocked_cleanly() {
let interp = interpreter();
let resp = interp.execute("print(eval(\"2+2\"))", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "eval should be blocked, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(!msg.contains("name 'name '"), "error should not be double-wrapped: {msg}");
assert!(msg.contains("eval"), "error should mention eval: {msg}");
}
#[tokio::test]
async fn exec_call_blocked_cleanly() {
let interp = interpreter();
let resp = interp.execute("exec(\"x=5\")\nprint(x)", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "exec should be blocked, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(!msg.contains("name 'name '"), "error should not be double-wrapped: {msg}");
assert!(msg.contains("exec"), "error should mention exec: {msg}");
}
#[tokio::test]
async fn engine_runtime_error_carries_line_number_single_statement() {
let interp = interpreter();
let resp = interp
.execute(
r#"x = "hello"
x()"#,
&no_tools(),
HashMap::new(),
)
.await;
assert!(resp.error.is_some(), "expected error, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(msg.contains("not callable"), "error should still name what went wrong: {msg}");
assert!(
msg.contains("line 2"),
"error must include source line so agent loops can self-correct: {msg}"
);
}
#[tokio::test]
async fn engine_runtime_error_points_at_the_offending_line() {
let interp = interpreter();
let resp = interp
.execute(
r#"a = 1
b = 2
c = "hello"
c()"#,
&no_tools(),
HashMap::new(),
)
.await;
assert!(resp.error.is_some(), "expected error, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(
msg.contains("line 4"),
"error must point at the offending line (4), not the file or nothing: {msg}"
);
}
#[tokio::test]
async fn engine_dict_get_as_key_fn_either_works_or_reports_line() {
let interp = interpreter();
let resp = interp
.execute(
r"monthly_data = {'A': 1, 'B': 2, 'C': 3}
highest = max(monthly_data, key=monthly_data.get)
print(highest)",
&no_tools(),
HashMap::new(),
)
.await;
if resp.error.is_none() {
assert_eq!(resp.stdout.trim(), "C");
} else {
let msg = err_msg(&resp);
assert!(
msg.contains("line 2"),
"if max(d, key=d.get) still errors, the message must include the source line so agent loops can self-correct: {msg}"
);
}
}
#[tokio::test]
async fn engine_lambda_body_error_stamps_lambda_line() {
let interp = interpreter();
let resp = interp.execute("f = lambda x: 1 / 0\nf(5)", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(
msg.contains("line 1"),
"error must point at the lambda body (line 1), not the call site: {msg}"
);
}
#[tokio::test]
async fn engine_function_body_error_stamps_inner_line() {
let interp = interpreter();
let resp = interp
.execute("def f():\n y = 1\n return y / 0\nf()", &no_tools(), HashMap::new())
.await;
assert!(resp.error.is_some(), "expected error, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(
msg.contains("line 3"),
"error must point at the failing return inside f (line 3), not the call site: {msg}"
);
}
#[tokio::test]
async fn engine_recursion_works_at_realistic_depth() {
let interp = interpreter();
let resp = interp
.execute(
"def f(n):\n if n <= 0:\n return 0\n return f(n - 1) + 1\nprint(f(8))",
&no_tools(),
HashMap::new(),
)
.await;
assert!(resp.error.is_none(), "8-level recursion must work on default stack: {:?}", resp.error);
assert_eq!(resp.stdout.trim(), "8");
}
#[tokio::test]
async fn engine_sentinel_lookalike_string_is_not_callable() {
let interp = interpreter();
let resp =
interp.execute("f = '__builtin__print'\nf('hello')", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "string variable must not dispatch as a builtin");
let msg = err_msg(&resp);
assert!(
msg.contains("'f' is not callable") || msg.contains("'str' object is not callable"),
"must error with a callable-type complaint: {msg}"
);
}
#[tokio::test]
async fn engine_lambda_survives_state_round_trip() {
let interp = interpreter();
let resp = interp.execute("f = lambda x: x * 3", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_none(), "definition should succeed: {:?}", resp.error);
let exported = interp.export_state().expect("export should succeed");
let interp2 = interpreter();
interp2.import_state(&exported).expect("import should succeed");
let call_resp = interp2.execute("print(f(7))", &no_tools(), HashMap::new()).await;
assert!(call_resp.error.is_none(), "cross-execute call: {:?}", call_resp.error);
assert_eq!(call_resp.stdout.trim(), "21");
}
#[tokio::test]
async fn engine_cross_execute_function_error_stamps_defined_line() {
let interp = interpreter();
let resp1 =
interp.execute("def divide(x):\n return x / 0", &no_tools(), HashMap::new()).await;
assert!(resp1.error.is_none(), "definition should succeed: {:?}", resp1.error);
let resp2 = interp.execute("divide(5)", &no_tools(), HashMap::new()).await;
assert!(resp2.error.is_some(), "expected error, got stdout: {:?}", resp2.stdout);
let msg = err_msg(&resp2);
assert!(
msg.contains("line 2"),
"error must point at the failing line inside the persisted function (line 2 of its def), not at line 1 of the call execute: {msg}"
);
}
#[tokio::test]
async fn engine_typeerror_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("1 + 'x'", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(msg.starts_with("TypeError: "), "must begin with CPython prefix `TypeError: `: {msg}");
}
#[tokio::test]
async fn engine_nameerror_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("undefined_name", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("NameError: name '"),
"must begin with CPython prefix `NameError: name '...`: {msg}"
);
}
#[tokio::test]
async fn engine_valueerror_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("int('abc')", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("ValueError: "),
"must begin with CPython prefix `ValueError: `: {msg}"
);
}
#[tokio::test]
async fn engine_jsondecodeerror_prefix_matches_cpython() {
let interp = interpreter();
let resp =
interp.execute("import json\njson.loads('{not json')", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("json.decoder.JSONDecodeError: "),
"must begin with CPython qualified subclass name: {msg}"
);
}
#[tokio::test]
async fn engine_attributeerror_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("(1).nonexistent_attr", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("AttributeError: "),
"must begin with CPython prefix `AttributeError: `: {msg}"
);
}
#[tokio::test]
async fn engine_assertionerror_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("assert False, 'boom'", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("AssertionError"),
"must begin with CPython prefix `AssertionError`: {msg}"
);
}
#[tokio::test]
async fn engine_syntaxerror_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("def (", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("SyntaxError: "),
"must begin with CPython prefix `SyntaxError: `: {msg}"
);
}
#[tokio::test]
async fn engine_zerodivision_prefix_matches_cpython() {
let interp = interpreter();
let resp = interp.execute("1 / 0", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("ZeroDivisionError: "),
"must begin with CPython prefix `ZeroDivisionError: `: {msg}"
);
}
#[tokio::test]
async fn engine_recursionerror_prefix_matches_cpython() {
let mut config = InterpreterConfig::default();
config.max_recursion_depth = 10;
let interp = Interpreter::new(InterpreterDeps { tools: Tools::new() }, config);
let resp = interp.execute("def f(): f()\nf()", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error");
let msg = err_msg(&resp);
assert!(
msg.starts_with("RecursionError: "),
"must begin with CPython prefix `RecursionError: `: {msg}"
);
}
#[tokio::test]
async fn engine_comprehension_element_error_stamped() {
let interp = interpreter();
let resp = interp
.execute("data = [1, 2, 3]\nresult = [x() for x in data]", &no_tools(), HashMap::new())
.await;
assert!(resp.error.is_some(), "expected error, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(
msg.contains("not callable") && msg.contains("line 2"),
"comprehension element error must include type + line: {msg}"
);
}
#[tokio::test]
async fn engine_json_loads_error_preserves_position() {
let interp = interpreter();
let resp =
interp.execute("import json\njson.loads('{not json')", &no_tools(), HashMap::new()).await;
assert!(resp.error.is_some(), "expected error, got stdout: {:?}", resp.stdout);
let msg = err_msg(&resp);
assert!(
msg.contains("line 1") && msg.contains("column"),
"json.loads error must carry the parser's line/column for agent-loop self-correction: {msg}"
);
}
#[tokio::test]
async fn re_sub_backreference_uses_python_syntax() {
assert_output(
r#"import re
print(re.sub(r"(\w)", r"\1x", "ab"))"#,
"axbx",
)
.await;
}
#[tokio::test]
async fn re_compile_is_unsupported() {
assert_error("import re\nre.compile(r\"\\d+\")").await;
}
#[test]
fn value_enum_footprint_is_documented() {
use interpretthis::Value;
use std::mem::size_of;
let sz = size_of::<Value>();
assert!(sz <= 160, "Value grew to {sz} bytes — re-check boxing of heavy variants");
}