Lines
100 %
Functions
Branches
//! End-to-end tests for `(catch-each items var body)` (ADR-0025).
//!
//! Each form goes through the full Session pipeline: compiler emits the
//! lambda + `__nomi_catch_each` import, host fn walks the chain in
//! Rust, per-item errors classify into `(err code msg)` cells, engine
//! deadlines (fuel / epoch) re-throw past the iteration boundary.
//! Result cells are heterogeneous `pair<anyref>` chains, which the host
//! renders as `<anyref>` placeholders. Script-side digestion via
//! `length` is the precise probe for cell count; the wire envelope
//! shape (`:value` vs `:error`) confirms whether script-raised errors
//! were captured (good) or bubbled past `catch-each` (bug). Engine
//! deadlines must surface as a top-level `:error`, never as a `:value`.
//! Per-cell content (`(ok x)` vs `(err code msg)`) is unit-tested in
//! `rpc::natives::catch_each::tests` against the host's
//! `err_code_and_message` directly — re-asserting it through the
//! placeholder-rendered wire would require a script-side downcast that
//! the static type system intentionally refuses.
use rpc::{ScriptCtx, ScriptLimits, Session};
use uuid::Uuid;
async fn rpc_response(form: &str) -> String {
let mut session = Session::new(ScriptCtx::new(Uuid::new_v4())).unwrap();
session.handle_form(&format!("(:id 7 :form {form})")).await
}
async fn rpc_response_with_fuel(form: &str, fuel: u64) -> String {
let ctx = ScriptCtx::new(Uuid::new_v4()).with_limits(ScriptLimits {
fuel,
max_memory_pages: 64,
});
let mut session = Session::new(ctx).unwrap();
session.handle_form(&format!("(:id 8 :form {form})")).await
fn extract_value(response: &str) -> &str {
response
.split_once(":value ")
.map(|(_, rest)| rest.trim_end_matches(')').trim())
.unwrap_or(response)
#[tokio::test(flavor = "current_thread")]
async fn empty_items_produces_zero_cells() {
let resp = rpc_response("(fold (lambda (acc cell) (+ acc 1)) 0 (catch-each (list) x x))").await;
assert!(!resp.contains(":error"), "{resp}");
assert_eq!(extract_value(&resp), "0");
async fn all_ok_produces_one_cell_per_input_item() {
// Items are fractional Numbers so `(list ...)` rides `pair<ratio>`;
// the body's `x` then binds at Ratio without crossing the int↔ratio
// boundary the type system forbids.
let resp = rpc_response(
"(fold (lambda (acc cell) (+ acc 1)) 0 (catch-each (list 11/10 21/10 31/10) x x))",
)
.await;
assert_eq!(extract_value(&resp), "3");
async fn mixed_outcomes_keep_cell_count_equal_to_input_count() {
// The body raises on item 2; the host catches it as an err cell
// and resumes the iteration. The wire envelope must be a `:value`,
// not an `:error`, and the chain length must be 3 — proof that the
// raise was captured rather than bubbled past `catch-each`.
"(fold (lambda (acc cell) (+ acc 1)) 0 \
(catch-each (list 11/10 21/10 31/10) x \
(if (= x 21/10) (error 'oops \"bad\") x)))",
async fn all_err_produces_one_cell_per_input_item() {
// The body's only path is `(error ...)` so its return type doesn't
// constrain the items list element type — any element type works.
(catch-each (list 11/10 21/10 31/10) x (error 'fail \"each\")))",
async fn script_raised_error_does_not_propagate_to_wire_envelope() {
// The single-element variant: a body that always raises must still
// produce a successful wire response — the captured error rides
// inside the result list, not as a top-level `:error`. This is the
// load-bearing invariant that lets a batch script walk N items and
// report per-item failures structurally.
let resp =
rpc_response(r#"(catch-each (list 11/10) x (error 'no-such-account "id=42"))"#).await;
assert!(
!resp.contains(":error"),
"script-raise must be captured, not bubbled: {resp}"
);
assert!(resp.contains(":value"), "{resp}");
async fn engine_deadline_propagates_past_catch_each() {
// A genuinely-runaway body must escape catch-each rather than being
// captured as an err cell. Self-recursion without a base case burns
// through the fuel budget; the wire surface is a top-level
// `:code runtime` envelope (OutOfFuel), not a `:value` chain. This
// is the rule that prevents a script from wrapping an infinite loop
// in catch-each to defeat the per-Session deadline.
let form = r#"(begin
(defun forever (n) (forever (+ n 1)))
(catch-each (list 11/10 21/10 31/10) x (forever x)))"#;
let resp = rpc_response_with_fuel(form, 50_000).await;
resp.contains(":error"),
"expected engine error, got: {resp}"
let unswallowed = resp.contains(":code runtime") || resp.contains(":code interrupted");
unswallowed,
"deadline must propagate, not appear as err cell: {resp}"