1
//! End-to-end Tier 3.2 boundary-shim verification on the **script-mode**
2
//! linker world (`ScriptExecutor` / `Linker<ExecutionState>`).
3
//!
4
//! A top-level `(error 'code "msg")` lowers to `throw $nomi_error`; the
5
//! compiler-emitted boundary `try_table` around `process` catches the
6
//! uncaught throw, reads the condition's code+message in wasm, and calls
7
//! the `__nomi_raise` host fn (registered in `define_host_functions`).
8
//! That host fn returns `Err` carrying the `__nomi_raise:CODE:MSG`
9
//! marker. The wasmtime error chain is preserved through
10
//! `HookError::WASM`, so `classify_runtime_error` recovers the structured
11
//! `EngineError::ScriptRaised { code, message }` — the script's own
12
//! symbol on the wire, byte-identical to the rpc-eval path.
13
//!
14
//! Before 3.2 `(error)` had no working path on this linker world at all.
15

            
16
use scripting::HookError;
17
use scripting::executor::ScriptExecutor;
18
use scripting::format::{ContextType, EntityType};
19
use scripting::runtime::{EngineError, classify_runtime_error};
20
use scripting::serializer::MemorySerializer;
21

            
22
use nomiscript::{Compiler, Program, Reader, SymbolTable};
23

            
24
1
fn compile_script_mode(src: &str) -> Vec<u8> {
25
1
    let program: Program = Reader::parse(src).expect("parse script source");
26
1
    let mut compiler = Compiler::with_host_fns(Vec::new());
27
1
    let mut symbols = SymbolTable::with_builtins_for_wasm();
28
1
    compiler
29
1
        .compile(&program, &mut symbols)
30
1
        .expect("(error) must compile + link in script mode post-3.2")
31
1
}
32

            
33
1
fn minimal_batch_input(output_size: u32) -> Vec<u8> {
34
1
    let mut ser = MemorySerializer::new();
35
1
    ser.set_context(ContextType::BatchProcess, EntityType::Transaction);
36
1
    ser.finalize(output_size)
37
1
}
38

            
39
#[test]
40
1
fn top_level_error_classifies_as_script_raised_with_code_and_message() {
41
1
    let wasm = compile_script_mode(r#"(error 'no-such-account "id=42")"#);
42
1
    let executor = ScriptExecutor::new();
43
1
    let input = minimal_batch_input(4096);
44

            
45
1
    let err = executor
46
1
        .execute(&wasm, &input, Some(4096))
47
1
        .expect_err("uncaught (error) must surface as an Err, not Ok");
48

            
49
1
    let HookError::WASM(wasm_err) = err else {
50
        panic!("expected HookError::WASM preserving the wasmtime chain, got {err:?}");
51
    };
52

            
53
1
    match classify_runtime_error(&wasm_err) {
54
1
        EngineError::ScriptRaised { code, message } => {
55
            // Symbols upcase through the reader (Lisp convention), so the
56
            // wire :code is the upper-cased form — byte-identical to the
57
            // rpc-eval path (`rpc/tests/parity.rs` asserts the same).
58
1
            assert_eq!(
59
                code, "NO-SUCH-ACCOUNT",
60
                "wire :code is the script's own (upcased) symbol"
61
            );
62
1
            assert_eq!(message, "id=42", "wire :message is the script's literal");
63
        }
64
        other => panic!("boundary bridge must classify as ScriptRaised, got {other:?}"),
65
    }
66
1
}