1
//! Diagnostic / session-introspection natives.
2
//!
3
//! Distinct from the 30 server-command bindings: these expose facts about
4
//! the rpc channel itself (protocol version, authenticated user) that don't
5
//! pass through `server::command::*`. Useful as a connection sanity check the
6
//! emacs client can call on connect to verify it's talking to the right
7
//! server, and as the smallest possible end-to-end proof that the host-fn
8
//! dispatch path through `SessionData` works before any DB-touching native
9
//! lands.
10

            
11
use scripting::runtime::alloc_string_ref;
12
use wasmtime::{ArrayRef, Caller, Linker, Rooted};
13

            
14
use crate::session::SessionData;
15

            
16
/// Wire-level protocol revision the server speaks. Bumped when an
17
/// incompatible change to the request/response envelope or capture protocol
18
/// ships. The emacs client can refuse to talk to a mismatched server.
19
pub const PROTOCOL_VERSION: i32 = 1;
20

            
21
/// Names exposed via `(<name>)` in nomiscript. Not counted in the plan's
22
/// 30 server-command surface — this is a separate, smaller diagnostic
23
/// surface that emacs / cli / tui all share.
24
pub const REGISTERED_NATIVES: &[&str] = &["rpc-protocol-version", "rpc-session-user-id"];
25

            
26
/// Returns the `HostFnSpec` list the nomiscript compiler needs in order to
27
/// recognise the meta natives during compilation. Mirrors the linker
28
/// registrations in [`register`] — both must stay in sync, hence one place.
29
2660
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
30
2660
    linker.func_wrap(
31
2660
        "nomi",
32
2660
        "rpc_protocol_version",
33
93
        |_caller: Caller<'_, SessionData>| -> i32 { PROTOCOL_VERSION },
34
    )?;
35
2660
    linker.func_wrap(
36
2660
        "nomi",
37
2660
        "rpc_session_user_id_capture",
38
        |mut caller: Caller<'_, SessionData>| -> wasmtime::Result<Option<Rooted<ArrayRef>>> {
39
            let uid_string = caller.data().ctx().user_id.to_string();
40
            Ok(Some(alloc_string_ref(&mut caller, uid_string.as_bytes())?))
41
        },
42
    )?;
43
2660
    Ok(())
44
2660
}
45

            
46
#[cfg(test)]
47
mod tests {
48
    use super::*;
49
    use crate::ctx::ScriptCtx;
50
    use scripting::runtime::{EngineOpts, build_engine, compile_wat};
51
    use uuid::Uuid;
52
    use wasmtime::Store;
53

            
54
1
    fn run_module_with_user(uid: Uuid, wat: &str) -> (SessionData, i32) {
55
1
        let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
56
1
        let module = compile_wat(&engine, wat).unwrap();
57
1
        let mut linker: Linker<SessionData> = Linker::new(&engine);
58
1
        register(&mut linker).unwrap();
59
1
        let mut store: Store<SessionData> = Store::new(
60
1
            &engine,
61
1
            SessionData::new(
62
1
                ScriptCtx::new(uid),
63
1
                std::sync::Arc::new(std::sync::Mutex::new(String::new())),
64
            ),
65
        );
66
1
        store.set_fuel(100_000).unwrap();
67
1
        store.set_epoch_deadline(1);
68
1
        let instance = linker.instantiate(&mut store, &module).unwrap();
69
1
        let func = instance
70
1
            .get_typed_func::<(), i32>(&mut store, "nomi-eval")
71
1
            .unwrap();
72
1
        let result = func.call(&mut store, ()).unwrap();
73
1
        (store.into_data(), result)
74
1
    }
75

            
76
    #[test]
77
1
    fn protocol_version_native_returns_constant() {
78
        // Smoke test for `rpc_protocol_version` — wat returns the i32
79
        // directly from nomi-eval so the test stays decoupled from the
80
        // anyref decoder that the rpc Session uses in production.
81
1
        let wat = r#"
82
1
        (module
83
1
          (import "nomi" "rpc_protocol_version" (func $ver (result i32)))
84
1
          (func (export "nomi-eval") (result i32) (call $ver)))
85
1
        "#;
86
1
        let (_, value) = run_module_with_user(Uuid::nil(), wat);
87
1
        assert_eq!(value, PROTOCOL_VERSION);
88
1
    }
89

            
90
    #[test]
91
1
    fn registered_natives_are_kebab_case() {
92
2
        for name in REGISTERED_NATIVES {
93
2
            assert!(
94
2
                name.chars()
95
39
                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
96
                "non-kebab-case meta native: {name}"
97
            );
98
        }
99
1
    }
100
}