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
This commit is contained in:
+19
-2
@@ -7,6 +7,23 @@ use serde_json::{Value, json};
|
||||
use tokio::sync::{mpsc, oneshot, watch};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn params_not_object(params: &Value) -> Value {
|
||||
let kind = match params {
|
||||
Value::String(_) => "a string",
|
||||
Value::Array(_) => "an array",
|
||||
Value::Number(_) => "a number",
|
||||
Value::Null => "null",
|
||||
Value::Bool(_) => "a boolean",
|
||||
Value::Object(_) => "an object",
|
||||
};
|
||||
error(
|
||||
-32602,
|
||||
&format!(
|
||||
"params must be a JSON object, got {kind}. Send the object itself, not a JSON-encoded string."
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// Queue an incoming response or notification for its destination.
|
||||
pub(crate) fn send(&self, pid: &str, value: Value) {
|
||||
@@ -69,7 +86,7 @@ impl Host {
|
||||
let host = self.clone();
|
||||
Box::pin(async move {
|
||||
if !params.is_object() {
|
||||
return Err(error(-32602, "params must be an object"));
|
||||
return Err(params_not_object(¶ms));
|
||||
}
|
||||
if caller != "console" && caller != "core" {
|
||||
let state = host.0.state.lock().unwrap();
|
||||
@@ -238,7 +255,7 @@ impl Host {
|
||||
// Deliver progress before a following response removes its destination.
|
||||
let params = v.get("params").cloned().unwrap_or(json!({}));
|
||||
let result = if !params.is_object() {
|
||||
Err(error(-32602, "params must be an object"))
|
||||
Err(params_not_object(¶ms))
|
||||
} else if method == "core.unlock" {
|
||||
if let Some(path) = params["path"].as_str() {
|
||||
self.unlock(pid, path);
|
||||
|
||||
+107
-2
@@ -500,14 +500,119 @@ async fn unimplemented_core_methods_return_method_not_found() {
|
||||
async fn params_must_be_objects() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
for (params, kind) in [
|
||||
(json!("{\"text\":\"hello\"}"), "a string"),
|
||||
(json!([]), "an array"),
|
||||
(json!(42), "a number"),
|
||||
(json!(null), "null"),
|
||||
(json!(true), "a boolean"),
|
||||
] {
|
||||
let expected = json!({
|
||||
"code": -32602,
|
||||
"message": format!("params must be a JSON object, got {kind}. Send the object itself, not a JSON-encoded string.")
|
||||
});
|
||||
for method in ["probe.echo", "core.ps"] {
|
||||
assert_eq!(h.call(method, params.clone()).await.unwrap_err(), expected);
|
||||
}
|
||||
assert_eq!(
|
||||
h.call("core.call", json!({"method":"probe.echo", "params":params}))
|
||||
.await
|
||||
.unwrap_err(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
let object = json!({"text":"hello"});
|
||||
assert_eq!(call(&h, "probe.echo", object.clone()).await, object);
|
||||
assert_eq!(
|
||||
h.call("probe.echo", json!([])).await.unwrap_err()["code"],
|
||||
-32602
|
||||
call(
|
||||
&h,
|
||||
"core.call",
|
||||
json!({"method":"probe.echo", "params":object})
|
||||
)
|
||||
.await,
|
||||
object
|
||||
);
|
||||
assert_eq!(
|
||||
call(&h, "core.call", json!({"method":"probe.echo"})).await,
|
||||
json!({})
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: stdout fast paths reject non-objects without changing defaults.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn stdout_params_must_be_objects() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let mut notifications = h.subscribe();
|
||||
for method in ["core.notify", "core.unlock", "probe.echo"] {
|
||||
for (params, kind) in [
|
||||
(json!("{}"), "a string"),
|
||||
(json!([]), "an array"),
|
||||
(json!(1.5), "a number"),
|
||||
(json!(null), "null"),
|
||||
(json!(false), "a boolean"),
|
||||
] {
|
||||
let response = call(
|
||||
&h,
|
||||
"probe.raw_call",
|
||||
json!({
|
||||
"jsonrpc":"2.0", "id":1, "method":method, "params":params
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response["id"], 1);
|
||||
assert_eq!(
|
||||
response["error"],
|
||||
json!({
|
||||
"code": -32602,
|
||||
"message": format!("params must be a JSON object, got {kind}. Send the object itself, not a JSON-encoded string.")
|
||||
})
|
||||
);
|
||||
}
|
||||
let params = match method {
|
||||
"core.notify" => json!({"name":"valid", "params":{"n":1}}),
|
||||
"core.unlock" => json!({"path":"/state/not-held"}),
|
||||
_ => json!({"n":1}),
|
||||
};
|
||||
let response = call(
|
||||
&h,
|
||||
"probe.raw_call",
|
||||
json!({
|
||||
"jsonrpc":"2.0", "id":1, "method":method, "params":params
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response["result"],
|
||||
if method == "probe.echo" {
|
||||
params
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
);
|
||||
assert!(response.get("error").is_none());
|
||||
let omitted = call(
|
||||
&h,
|
||||
"probe.raw_call",
|
||||
json!({
|
||||
"jsonrpc":"2.0", "id":1, "method":method
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
match method {
|
||||
"core.notify" => {
|
||||
assert_eq!(omitted["error"]["message"], "name must be a string");
|
||||
assert_eq!(notifications.try_recv().unwrap()["params"]["name"], "valid");
|
||||
}
|
||||
"core.unlock" => assert_eq!(omitted["error"]["message"], "path must be a string"),
|
||||
_ => assert_eq!(omitted["result"], json!({})),
|
||||
}
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.9: exit without answer reports status and last logs.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exit_without_answer_reports_status_and_last_logs() {
|
||||
|
||||
@@ -470,6 +470,10 @@ fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
|
||||
"network" => network(params),
|
||||
"read" => read_file(params),
|
||||
"write" => write_file(params),
|
||||
"raw_call" => {
|
||||
send(params.clone());
|
||||
receive(lines)
|
||||
}
|
||||
"call" => outgoing_call(params, lines),
|
||||
"lock" => lock(params, lines),
|
||||
_ => {
|
||||
|
||||
Reference in New Issue
Block a user