1
//! P4 A1 invariant tests.
2
//!
3
//! Locks in the structural guarantees of the universal-result
4
//! convention so a future refactor can't silently regress them:
5
//!
6
//! - Every registered `HostFnSpec` declares its result/params through
7
//!   `WasmType` alone — no per-shape boolean flags survive on the
8
//!   struct (verified by absence at compile time).
9
//! - Host fns returning typed GC refs (Ratio / Commodity / StringRef /
10
//!   PairRef / EntityRef) declare their import signature with the
11
//!   matching abstract heap-type so wasmtime's `Rooted<...>` /
12
//!   `WasmTy::valtype()` mapping holds.
13
//!
14
//! The "drift detector" test that used to live here retired when the
15
//! registry source-of-truth moved into
16
//! =doc/scripting/native_reference.org= (tangled by `rpc/build.rs`).
17
//! There's nothing to drift against — the org IS the source.
18

            
19
use nomiscript::{EntityKind, HostFnSpec, PairElement, WasmType};
20
use rpc::natives::all_compiler_specs;
21

            
22
/// Verifies every typed-result spec maps through the universal-result
23
/// convention to a non-empty wasm import signature. Each `WasmType`
24
/// variant is exercised at least once across the registry, so this
25
/// catches a future variant addition that forgets its handling in
26
/// `host_import_return_type`.
27
#[test]
28
1
fn every_wasm_type_variant_appears_in_registry() {
29
1
    let specs = all_compiler_specs();
30
1
    let mut seen_i32 = false;
31
1
    let mut seen_ratio = false;
32
1
    let mut seen_commodity = false;
33
1
    let mut seen_string = false;
34
1
    let mut seen_pair = false;
35
1
    let mut seen_entity = false;
36
44
    for spec in &specs {
37
44
        match spec.result {
38
14
            Some(WasmType::I32) => seen_i32 = true,
39
            // Bool is produced by comparison/predicate operators, not by the
40
            // typed-entity native registry this test surveys.
41
            Some(WasmType::Bool) => {}
42
1
            Some(WasmType::Ratio) => seen_ratio = true,
43
2
            Some(WasmType::Commodity) => seen_commodity = true,
44
14
            Some(WasmType::StringRef) => seen_string = true,
45
8
            Some(WasmType::PairRef(_)) => seen_pair = true,
46
5
            Some(WasmType::EntityRef(_)) => seen_entity = true,
47
            Some(WasmType::Closure(_)) => {}
48
            Some(WasmType::AnyRef) => {}
49
            None => {}
50
        }
51
    }
52
1
    assert!(seen_i32, "registry has no I32-returning native");
53
1
    assert!(seen_ratio, "registry has no Ratio-returning native");
54
1
    assert!(seen_commodity, "registry has no Commodity-returning native");
55
1
    assert!(seen_string, "registry has no StringRef-returning native");
56
1
    assert!(seen_pair, "registry has no PairRef-returning native");
57
1
    assert!(seen_entity, "registry has no EntityRef-returning native");
58
1
}
59

            
60
/// `account-balance` is the canonical commodity-bearing native — it
61
/// must surface a `Commodity` return so callers can compose with `+`,
62
/// `convert-commodity`, etc. Locks in the P3b/1b migration.
63
#[test]
64
1
fn account_balance_returns_commodity() {
65
1
    let specs = all_compiler_specs();
66
1
    let spec = specs
67
1
        .iter()
68
12
        .find(|s| s.nomi_name == "ACCOUNT-BALANCE")
69
1
        .expect("ACCOUNT-BALANCE missing from registry");
70
1
    assert_eq!(spec.result, Some(WasmType::Commodity));
71
1
    assert_eq!(spec.params, vec![WasmType::StringRef]);
72
1
}
73

            
74
/// `convert-commodity` is the cross-currency bridge — takes a typed
75
/// `Commodity` value and a target uuid string, returns a `Commodity`
76
/// in the target. Locks in the P3b/1d signature.
77
#[test]
78
1
fn convert_commodity_signature_holds() {
79
1
    let specs = all_compiler_specs();
80
1
    let spec = specs
81
1
        .iter()
82
16
        .find(|s| s.nomi_name == "CONVERT-COMMODITY")
83
1
        .expect("CONVERT-COMMODITY missing from registry");
84
1
    assert_eq!(spec.result, Some(WasmType::Commodity));
85
1
    assert_eq!(
86
        spec.params,
87
1
        vec![WasmType::Commodity, WasmType::StringRef],
88
        "convert-commodity must take (commodity, string)"
89
    );
90
1
}
91

            
92
/// Builder methods on `HostFnSpec` should cover the universal-result
93
/// convention end-to-end: chain `returns(...)` + `with_params(...)`
94
/// produces a structurally valid spec the registry can consume.
95
#[test]
96
1
fn host_fn_spec_builder_round_trip() {
97
1
    let spec = HostFnSpec::new("my-fn", "nomi", "my_fn")
98
1
        .with_params(vec![WasmType::I32, WasmType::StringRef])
99
1
        .returns(WasmType::PairRef(PairElement::Entity(EntityKind::Account)));
100
1
    assert_eq!(spec.nomi_name, "MY-FN");
101
1
    assert_eq!(spec.import_module, "nomi");
102
1
    assert_eq!(spec.import_name, "my_fn");
103
1
    assert_eq!(spec.params, vec![WasmType::I32, WasmType::StringRef]);
104
1
    assert!(matches!(
105
1
        spec.result,
106
        Some(WasmType::PairRef(PairElement::Entity(EntityKind::Account)))
107
    ));
108
1
}
109

            
110
/// Sanity check: list-X natives must return `PairRef(...)`. The shape
111
/// is structural — if `list-accounts` started returning `StringRef`,
112
/// every `(car (list-accounts))` composition would silently break,
113
/// since the typed CAR helper only accepts `PairRef`.
114
#[test]
115
1
fn list_natives_return_pair_ref() {
116
1
    let specs = all_compiler_specs();
117
44
    for spec in &specs {
118
44
        if spec.nomi_name.starts_with("LIST-") {
119
7
            assert!(
120
7
                matches!(spec.result, Some(WasmType::PairRef(_))),
121
                "{} must return PairRef, got {:?}",
122
                spec.nomi_name,
123
                spec.result
124
            );
125
37
        }
126
    }
127
1
}