fix(cr-2026-04-22): list_transcripts accepts omitted arguments

Regression surfaced by the batch review: commit 8400128 switched
list_transcripts from unwrap_or_default to map_err(-32602). This
correctly errors on malformed payloads but also rejected the common
case where a client omits the 'arguments' field entirely — which
arrives as Value::Null, and serde_json::from_value does not
deserialise Null into a struct.

Short-circuits the Null case to Args::default() before attempting
deserialisation. Genuine shape mismatches ("limit": "twenty")
still return -32602 as the previous test asserts.

New regression test: tools/call with list_transcripts and no
arguments key must return a successful response.
This commit is contained in:
2026-04-22 09:23:17 +01:00
parent b376b98f33
commit a5bc45e847

View File

@@ -191,13 +191,19 @@ async fn list_transcripts_tool(pool: &SqlitePool, args: Value) -> Result<Value,
#[serde(default)]
limit: Option<i64>,
}
// Every other tool handler returns -32602 on malformed args;
// previously this one silently fell back to defaults, hiding
// client bugs (2026-04-22 review MAJOR). An empty Value
// deserialises cleanly into Args::default, so the "no args"
// path still works — only genuinely malformed payloads error.
let args: Args = serde_json::from_value(args)
.map_err(|e| error(-32602, format!("Invalid arguments: {e}")))?;
// The `arguments` field in CallParams defaults to `Value::Null`
// when a client omits it entirely. `serde_json::from_value` does
// not accept Null as an empty object, so we short-circuit that
// case before deserialising — a missing `arguments` still falls
// back to defaults (the common case for list_transcripts), while
// a genuinely malformed payload returns -32602 per the Invalid
// arguments contract the other handlers use.
let args: Args = if args.is_null() {
Args::default()
} else {
serde_json::from_value(args)
.map_err(|e| error(-32602, format!("Invalid arguments: {e}")))?
};
let limit = args.limit.unwrap_or(20).clamp(1, 200);
let rows = kon_storage::list_transcripts(pool, limit)
@@ -426,6 +432,34 @@ mod tests {
assert!(err.message.contains("expected value"));
}
#[tokio::test]
async fn list_transcripts_accepts_omitted_arguments() {
// Regression for the review-of-review: tools/call requests
// that omit `arguments` arrive with `Value::Null`. The
// malformed-params fix must not reject those — it is the
// common shape for an empty call, equivalent to defaults.
let request = json!({
"jsonrpc": "2.0",
"id": 98,
"method": "tools/call",
"params": {
"name": "list_transcripts",
// `arguments` omitted
},
});
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
kon_storage::migrations::run_migrations(&pool).await.unwrap();
let response = handle_message(&pool, request).await.expect("has response");
assert!(
response.error.is_none(),
"omitted arguments must not error, got: {:?}",
response.error
);
assert!(response.result.is_some());
}
#[tokio::test]
async fn list_transcripts_rejects_malformed_params_with_invalid_arguments() {
// Regression for the 2026-04-22 review MAJOR: previously the