Give the local port's response id the same null tolerance as tool_calls (PA.f23, PA.f24)
PA.f23: LocalChatResponse.id already tolerated an absent field through #[serde(default)], but not an explicit "id": null, since #[serde(default)] only fires when the key is missing. Give it the same null_as_default treatment tool_calls already has, with a_null_response_id_decodes_as_empty mirroring the existing tool_calls coverage. PA.f24: add ipv6_loopback_opens_and_a_non_loopback_ipv6_address_is_refused, pinning that the loopback check is IP-address loopback and not "any IPv6 address": [::1] opens and a routable IPv6 address such as [2001:db8::1]:8080 is refused the same way a non-loopback IPv4 address is. Test-only; the loopback check itself was already correct. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c97fad2483
commit
6e7c73bdc4
1 changed files with 52 additions and 1 deletions
|
|
@ -61,7 +61,10 @@ struct LocalChatResponse {
|
|||
/// Used only for receipt bookkeeping (`response_id`), so an absent id is
|
||||
/// tolerated rather than an integrity violation; a missing or duplicate
|
||||
/// tool call id, which the tool loop must correlate against, still is.
|
||||
#[serde(default)]
|
||||
/// `null_as_default` covers an explicit `"id": null` the same way it
|
||||
/// does for `tool_calls`: `#[serde(default)]` alone only fires when the
|
||||
/// key is missing, not when it is present and null.
|
||||
#[serde(default, deserialize_with = "null_as_default")]
|
||||
id: String,
|
||||
choices: Vec<LocalChatChoice>,
|
||||
#[serde(default)]
|
||||
|
|
@ -772,6 +775,36 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
/// PA.f24: the loopback check is IP-address loopback, not "any IPv6
|
||||
/// address" or "any address that parses." `[::1]` — IPv6 loopback —
|
||||
/// opens, and a routable IPv6 address such as `[2001:db8::1]:8080` (a
|
||||
/// documentation-range address; never loopback) is refused the same
|
||||
/// way a non-loopback IPv4 address is.
|
||||
#[test]
|
||||
fn ipv6_loopback_opens_and_a_non_loopback_ipv6_address_is_refused() {
|
||||
let _guard = client_build_lock()
|
||||
.lock()
|
||||
.expect("the client-build lock is never poisoned");
|
||||
LocalInferencePort::new(
|
||||
"[::1]:1".parse().unwrap(),
|
||||
DEFAULT_LOCAL_MODEL_PREFIX,
|
||||
TEST_RUNTIME,
|
||||
)
|
||||
.expect("the IPv6 loopback address opens");
|
||||
|
||||
let error = LocalInferencePort::new(
|
||||
"[2001:db8::1]:8080".parse().unwrap(),
|
||||
DEFAULT_LOCAL_MODEL_PREFIX,
|
||||
TEST_RUNTIME,
|
||||
)
|
||||
.err()
|
||||
.expect("a non-loopback IPv6 endpoint opened");
|
||||
assert!(matches!(
|
||||
error,
|
||||
ControllerOpenError::LocalEndpointNotLoopback { .. }
|
||||
));
|
||||
}
|
||||
|
||||
/// Spec test.
|
||||
#[tokio::test]
|
||||
async fn a_call_to_an_unoffered_tool_is_an_integrity_violation() {
|
||||
|
|
@ -1089,6 +1122,24 @@ mod tests {
|
|||
assert_eq!(response.id, "");
|
||||
}
|
||||
|
||||
/// PA.f23: an explicit `"id": null` decodes to the empty string the
|
||||
/// same way an absent `id` does, instead of failing to decode. Mirrors
|
||||
/// `a_null_tool_calls_field_decodes_as_no_calls`.
|
||||
#[test]
|
||||
fn a_null_response_id_decodes_as_empty() {
|
||||
let raw = json!({
|
||||
"id": Value::Null,
|
||||
"choices": [{
|
||||
"message": {"role": "assistant", "content": "ok", "tool_calls": []},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
})
|
||||
.to_string();
|
||||
let response: LocalChatResponse =
|
||||
serde_json::from_str(&raw).expect("a null id decodes");
|
||||
assert_eq!(response.id, "");
|
||||
}
|
||||
|
||||
/// Spec test (PA.f19). Open refuses an empty local model prefix: an empty
|
||||
/// prefix matches every model and would silently take the local port's
|
||||
/// lanes.
|
||||
|
|
|
|||
Loading…
Reference in a new issue