use aethershell::value::Value;
use std::collections::BTreeMap;
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
fn lock() -> std::sync::MutexGuard<'static, ()> {
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn rec(pairs: &[(&str, Value)]) -> Value {
let mut m = BTreeMap::new();
for (k, v) in pairs {
m.insert(k.to_string(), v.clone());
}
Value::Record(m)
}
fn call(name: &str, args: Vec<Value>) -> Value {
let mut env = aethershell::env::Env::new();
aethershell::builtins::call(name, args, &mut env).expect("builtin call")
}
fn try_call(name: &str, args: Vec<Value>) -> Result<Value, String> {
let mut env = aethershell::env::Env::new();
aethershell::builtins::call(name, args, &mut env).map_err(|e| e.to_string())
}
fn big_table(n: i64) -> Value {
Value::Array(
(0..n)
.map(|i| {
rec(&[
("id", Value::Int(1000 + i)),
(
"path",
Value::Str(format!("/srv/app/module_{i:03}/main.rs")),
),
(
"status",
Value::Str(if i % 3 == 0 { "ok" } else { "stale" }.into()),
),
("bytes", Value::Int(4096 + i * 7)),
])
})
.collect(),
)
}
fn render(v: &Value) -> String {
aethershell::builtins::render_agent(v, None).expect("agent render")
}
#[test]
fn a_large_result_is_rendered_as_a_handle_not_as_data() {
let _g = lock();
aethershell::handles::clear();
let out = render(&big_table(400));
assert!(out.starts_with("@handle "), "expected a handle:\n{out}");
assert!(out.contains("@shape array<record{"), "shape stated:\n{out}");
assert!(out.contains("@items 400"), "size stated:\n{out}");
assert!(out.contains("@hint handle("), "usage taught inline:\n{out}");
}
#[test]
fn the_handle_is_lossless() {
let _g = lock();
aethershell::handles::clear();
let original = big_table(400);
render(&original);
let id = aethershell::handles::list()[0].id.clone();
let recovered = call("handle", vec![Value::Str(id)]);
assert_eq!(
recovered, original,
"a handle must return exactly what was computed"
);
}
fn tokens(s: &str) -> usize {
match call("tokens", vec![Value::Str(s.to_string())]) {
Value::Int(n) => n as usize,
Value::Record(m) => match m.get("tokens") {
Some(Value::Int(n)) => *n as usize,
other => panic!("unexpected tokens shape: {other:?}"),
},
other => panic!("unexpected tokens result: {other:?}"),
}
}
#[test]
fn the_handle_costs_far_less_than_the_data_and_the_gap_widens_with_size() {
let _g = lock();
let mut ratios = Vec::new();
for n in [400, 2000] {
aethershell::handles::clear();
let v = big_table(n);
let whole = match call("aecon", vec![v.clone()]) {
Value::Str(s) => s,
other => panic!("aecon should return a string, got {other:?}"),
};
let handled = render(&v);
let (wt, ht) = (tokens(&whole), tokens(&handled));
println!(
"{n} rows — whole: {wt} tokens; handle: {ht} tokens ({:.1}x)",
wt as f64 / ht as f64
);
assert!(
ht * 8 < wt,
"{n} rows: expected a large saving, {ht} vs {wt}"
);
ratios.push(wt as f64 / ht as f64);
}
assert!(
ratios[1] > ratios[0],
"the saving must grow with the result: {ratios:?}"
);
}
#[test]
fn a_preview_states_how_much_it_omits() {
let _g = lock();
aethershell::handles::clear();
let out = render(&big_table(400));
assert!(out.contains("@omitted 397"), "omission stated:\n{out}");
}
#[test]
fn a_small_result_is_returned_whole() {
let _g = lock();
aethershell::handles::clear();
let small = big_table(3);
let out = render(&small);
assert!(
!out.starts_with("@handle"),
"small results stay whole:\n{out}"
);
assert!(
aethershell::handles::list().is_empty(),
"a small result must not consume a handle"
);
}
#[test]
fn a_giant_string_is_not_handled_because_it_could_not_be_narrowed() {
let _g = lock();
aethershell::handles::clear();
let out = render(&Value::Array(vec![Value::Str("x".repeat(50_000))]));
let _ = out;
let huge = Value::Str("x".repeat(50_000));
let rendered = aethershell::builtins::render_agent(&huge, None).expect("render");
assert!(
!rendered.starts_with("@handle"),
"an opaque blob is sent as-is"
);
}
#[test]
fn handles_lists_what_is_live_without_the_data() {
let _g = lock();
aethershell::handles::clear();
render(&big_table(400));
render(&big_table(500));
let listed = call("handles", vec![]);
match listed {
Value::Array(rows) => {
assert_eq!(rows.len(), 2);
let first = format!("{:?}", rows[0]);
assert!(first.contains("items"), "listing carries size: {first}");
assert!(
!first.contains("module_001"),
"the listing must not re-send the data: {first}"
);
}
other => panic!("expected an array, got {other:?}"),
}
}
#[test]
fn an_unknown_handle_is_an_actionable_error_not_a_null() {
let _g = lock();
aethershell::handles::clear();
let err = try_call("handle", vec![Value::Str("h99".into())])
.expect_err("an unknown handle must error");
assert!(err.contains("h99"), "names the id: {err}");
assert!(
err.contains("no handles live") || err.contains("live:"),
"explains what is available: {err}"
);
}
#[test]
fn dropping_a_handle_reports_whether_it_existed() {
let _g = lock();
aethershell::handles::clear();
render(&big_table(400));
let id = aethershell::handles::list()[0].id.clone();
assert_eq!(
call("handle_drop", vec![Value::Str(id.clone())]),
Value::Bool(true)
);
assert_eq!(
call("handle_drop", vec![Value::Str(id)]),
Value::Bool(false)
);
}
#[test]
fn a_handle_survives_into_a_separate_process() {
let _g = lock();
let dir = std::env::temp_dir().join(format!("ae_xproc_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("session dir");
let ae = env!("CARGO_BIN_EXE_ae");
let run = |code: &str| -> String {
let out = std::process::Command::new(ae)
.args(["--agent", "-c", code])
.env("AETHER_SESSION_DIR", &dir)
.output()
.expect("run ae");
String::from_utf8_lossy(&out.stdout).into_owned()
};
let produced = run("range(1, 600) | map(fn(i) => { n: i, m: i * 7919 })");
assert!(
produced.contains("@handle "),
"process 1 should return a handle, got:
{produced}"
);
let id = produced
.lines()
.find_map(|l| l.strip_prefix("@handle "))
.expect("a handle id")
.trim()
.to_string();
let resolved = run(&format!("handle(\"{id}\") | len()"));
let _ = std::fs::remove_dir_all(&dir);
assert!(
resolved.trim() == "599",
"a second process must resolve the handle, got:
{resolved}"
);
}