Files
SAU-317 implementer c54edea596
ci / check (push) Failing after 2m38s
Name the received JSON type in non-object params errors
Keep -32602 and existing validation order; tell callers to send the object itself.
Cover host routing, stdout fast paths, SDK calls and generated dispatch.

Card: SAU-317
2026-10-28 19:16:39 +00:00

187 lines
5.2 KiB
Rust

//! Native checks for generated schemas, dispatch, and missing core transports.
use sausage_plugin::serde_json::json;
use sausage_plugin::{Ctx, Interface, Result, rpc, schema};
/// The addition parameters.
#[schema]
struct Add {
/// The first integer.
a: i32,
/// The second integer.
b: i32,
}
/// A type with different serialize and deserialize contracts.
#[schema]
struct Defaulted {
/// A value which the provider defaults.
#[serde(default)]
value: String,
}
/// A documented string enum.
#[schema]
enum Choice {
/// The first choice.
First,
/// The second choice.
Second,
}
struct Plugin;
#[rpc(endpoints(control=Plugin))]
impl Plugin {
/// Return the same named object without a contract change.
fn identity(&self, p: Add) -> Result<Add> {
Ok(p)
}
/// Choose the first value.
fn choice(&self) -> Result<Choice> {
Ok(Choice::First)
}
/// Round-trip the defaulted object.
fn defaults(&self, p: Defaulted) -> Result<Defaulted> {
Ok(p)
}
/// Add integers.
///
/// Return their sum.
fn add(&self, p: Add) -> Result<i32> {
Ok(p.a + p.b)
}
}
#[test]
fn discovery_and_dispatch() {
let doc = Plugin.discover();
assert!(doc["components"]["schemas"].get("Empty").is_none());
let choice = doc["methods"]
.as_array()
.unwrap()
.iter()
.find(|m| m["name"] == "choice")
.unwrap();
assert!(choice.get("x-sausage-params").is_none());
assert!(
doc["x-sausage-endpoints"]["control"]["methods"]
.as_array()
.unwrap()
.iter()
.any(|m| m["name"] == "add")
);
assert!(doc["components"]["schemas"].get("AddResult").is_none());
assert_eq!(
doc["methods"]
.as_array()
.unwrap()
.iter()
.find(|m| m["name"] == "identity")
.unwrap()["result"]["schema"]["$ref"],
"#/components/schemas/Add"
);
assert!(
doc["components"]["schemas"]["Defaulted"]
.get("required")
.is_none()
);
assert_eq!(
doc["components"]["schemas"]["DefaultedResult"]["required"],
json!(["value"])
);
assert_eq!(
doc["components"]["schemas"]["Choice"]["oneOf"][0]["description"],
json!("The first choice.")
);
let add = doc["methods"]
.as_array()
.unwrap()
.iter()
.find(|m| m["name"] == "add")
.unwrap();
assert_eq!(add["summary"], "Add integers.");
assert_eq!(
doc["components"]["schemas"]["Add"]["description"],
"The addition parameters."
);
assert_eq!(add["params"][0]["description"], "The first integer.");
eprintln!("DISCOVER={doc}");
assert_eq!(
sausage_plugin::block_on(Plugin.dispatch("add", json!({"a":2,"b":3}), &Ctx::default()))
.unwrap(),
5
);
assert_eq!(
sausage_plugin::block_on(Plugin.dispatch("add", json!({}), &Ctx::default()))
.unwrap_err()
.code,
-32602
);
let internal: sausage_plugin::Error = sausage_plugin::serde_json::from_str::<i32>("bad")
.unwrap_err()
.into();
assert_eq!(internal.code, -32603);
}
#[sausage_plugin::test]
async fn params_must_be_objects() {
for (params, kind) in [
(json!("{\"a\":2,\"b\":3}"), "a string"),
(json!([]), "an array"),
(json!(42), "a number"),
(json!(null), "null"),
(json!(true), "a boolean"),
] {
let message = format!(
"params must be a JSON object, got {kind}. Send the object itself, not a JSON-encoded string."
);
let outgoing = sausage_plugin::call("plugin.add", params.clone())
.await
.unwrap_err();
assert_eq!(outgoing.code, -32602);
assert_eq!(outgoing.message, message);
for method in ["add", "choice", "unknown"] {
let incoming = Plugin
.dispatch(method, params.clone(), &Ctx::default())
.await
.unwrap_err();
assert_eq!(incoming.code, -32602);
assert_eq!(incoming.message, message);
}
}
assert_eq!(
Plugin
.dispatch("add", json!({"a":2,"b":3}), &Ctx::default())
.await
.unwrap(),
5
);
assert_eq!(
Plugin
.dispatch("defaults", json!({}), &Ctx::default())
.await
.unwrap(),
json!({"value":""})
);
// An object reaches transport validation rather than the params rejection.
let outgoing = sausage_plugin::call("plugin.add", json!({"a":2,"b":3}))
.await
.unwrap_err();
assert!(outgoing.message.contains("no core transport"));
}
#[sausage_plugin::test]
async fn missing_core_fails_without_wait() {
assert!(
Ctx::default()
.notify("progress", json!({}))
.unwrap_err()
.message
.contains("no core transport")
);
assert!(
sausage_plugin::call("core.lock", json!({}))
.await
.unwrap_err()
.message
.contains("no core transport")
);
}
#[test]
fn compile_fail() {
trybuild::TestCases::new().compile_fail("tests/ui/*.rs");
}