fix(server): hyper knobs, decoy fidelity, cache + builder fixes (SRV-04..SRV-10)
- SRV-04: TokioTimer on h1+h2 builder, header_read_timeout 10s, h1 keep-alive on, h2 keep-alive 30s/10s; concurrency boundary documented - SRV-05: with_decoy rebuild keeps extra routes (clone, not take) - SRV-07: method_not_allowed_fallback serves the nginx-shaped 405 - SRV-08: UTF-8 percent-decoding, literal '+', tokio::fs syscalls - SRV-09: /openapi.json cached at construction, generic 500 body, to_openapi returns Result (expect removed) - SRV-10: ChannelsPolicy extension injection point on the WS upgrade; single token resolution via route ordering (WS layer before the router-wide auth route_layer) Verification: cargo test 265 passed; --all-features server::/to_openapi:: green; clippy + fmt clean on touched files (remaining tree noise is a parallel agent's in-flight from_mcp/from_wss/forward work)
This commit is contained in:
+30
-21
@@ -29,6 +29,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use alkcall::client::AdapterError;
|
||||
use alkcall::registry::registration::OperationRegistry;
|
||||
use alkcall::registry::spec::ErrorDefinition;
|
||||
|
||||
@@ -61,10 +62,18 @@ const CODE_TIMEOUT: &str = "TIMEOUT";
|
||||
|
||||
const HTTP_PREFIX: &str = "HTTP_";
|
||||
|
||||
pub fn to_openapi(registry: &OperationRegistry) -> OpenAPISpec {
|
||||
/// Project the registry into the fixed 6-endpoint gateway doc (ADR-042).
|
||||
///
|
||||
/// Returns [`AdapterError::SchemaParse`] if the generated doc does not
|
||||
/// re-validate against the structural checks in `OpenAPISpec::from_value`
|
||||
/// — a would-be invariant violation of `build_doc`, not a caller-facing
|
||||
/// input error. The HTTP surface serves only a generic `500` for this
|
||||
/// (`/openapi.json` handler caches the serialized doc; SRV-09): no
|
||||
/// serde/parse internals reach the wire.
|
||||
pub fn to_openapi(registry: &OperationRegistry) -> Result<OpenAPISpec, AdapterError> {
|
||||
let operation_errors = collect_operation_errors(registry);
|
||||
let raw = build_doc(operation_errors);
|
||||
OpenAPISpec::from_value(raw).expect("to_openapi always emits a valid OpenAPI document")
|
||||
OpenAPISpec::from_value(raw)
|
||||
}
|
||||
|
||||
fn build_doc(operation_errors: Vec<ErrorDefinition>) -> Value {
|
||||
@@ -659,7 +668,7 @@ mod tests {
|
||||
#[test]
|
||||
fn empty_registry_produces_six_gateway_paths() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let paths = paths_object(&spec);
|
||||
assert_eq!(paths.len(), 6);
|
||||
assert!(paths.contains_key(PATH_SEARCH));
|
||||
@@ -675,7 +684,7 @@ mod tests {
|
||||
let mut registry = OperationRegistry::new();
|
||||
register(&mut registry, external_spec("fs/readFile", vec![]));
|
||||
register(&mut registry, external_spec("agent/chat", vec![]));
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let paths = paths_object(&spec);
|
||||
assert_eq!(paths.len(), 6);
|
||||
assert!(!paths.contains_key("/fs/readFile"));
|
||||
@@ -685,7 +694,7 @@ mod tests {
|
||||
#[test]
|
||||
fn info_version_is_1_1_0_after_publish_addition() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let version = spec
|
||||
.raw
|
||||
.get("info")
|
||||
@@ -702,7 +711,7 @@ mod tests {
|
||||
#[test]
|
||||
fn info_title_present() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let title = spec
|
||||
.raw
|
||||
.get("info")
|
||||
@@ -715,7 +724,7 @@ mod tests {
|
||||
#[test]
|
||||
fn openapi_field_is_3_0_0() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let openapi = spec.raw.get("openapi").and_then(Value::as_str).unwrap();
|
||||
assert_eq!(openapi, OPENAPI_VERSION);
|
||||
}
|
||||
@@ -723,7 +732,7 @@ mod tests {
|
||||
#[test]
|
||||
fn publish_has_post_method_with_ndjson_request_body() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
assert!(path(&spec, PATH_PUBLISH).contains_key("post"));
|
||||
let request_schema = operation(&spec, PATH_PUBLISH, "post")
|
||||
.get("requestBody")
|
||||
@@ -747,7 +756,7 @@ mod tests {
|
||||
#[test]
|
||||
fn publish_includes_protocol_error_statuses_including_400() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_PUBLISH, "post");
|
||||
for status in [
|
||||
STATUS_BAD_REQUEST,
|
||||
@@ -768,7 +777,7 @@ mod tests {
|
||||
#[test]
|
||||
fn publish_400_response_covers_invalid_input_and_invalid_operation_type() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_PUBLISH, "post");
|
||||
let schema = responses
|
||||
.get(&STATUS_BAD_REQUEST.to_string())
|
||||
@@ -799,7 +808,7 @@ mod tests {
|
||||
#[test]
|
||||
fn call_request_body_is_flat_operation_input() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let request_schema = operation(&spec, PATH_CALL, "post")
|
||||
.get("requestBody")
|
||||
.and_then(|rb| rb.get("content"))
|
||||
@@ -832,7 +841,7 @@ mod tests {
|
||||
#[test]
|
||||
fn call_includes_all_protocol_level_error_statuses() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
for status in [
|
||||
STATUS_BAD_REQUEST,
|
||||
@@ -853,7 +862,7 @@ mod tests {
|
||||
#[test]
|
||||
fn call_protocol_error_status_codes_have_protocol_codes() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
|
||||
let invalid_input_schema = responses
|
||||
@@ -906,7 +915,7 @@ mod tests {
|
||||
],
|
||||
),
|
||||
);
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
assert!(
|
||||
responses.contains_key("429"),
|
||||
@@ -934,7 +943,7 @@ mod tests {
|
||||
&mut registry,
|
||||
external_spec("svc/op", vec![error("HTTP_404", Some(404))]),
|
||||
);
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
let response_404 = responses.get("404").unwrap();
|
||||
let schema = response_404
|
||||
@@ -963,7 +972,7 @@ mod tests {
|
||||
&mut registry,
|
||||
external_spec("svc/op", vec![error("HTTP_404", Some(404))]),
|
||||
);
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
let response_404 = responses.get("404").unwrap();
|
||||
let schema = response_404
|
||||
@@ -1006,7 +1015,7 @@ mod tests {
|
||||
&mut registry,
|
||||
external_spec("svc/op", vec![error("SOME_ERROR", None)]),
|
||||
);
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
assert!(
|
||||
responses.len() < 10,
|
||||
@@ -1025,7 +1034,7 @@ mod tests {
|
||||
&mut registry,
|
||||
external_spec("svc/b", vec![error("TOO_MANY_REQUESTS", Some(429))]),
|
||||
);
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
assert!(responses.contains_key("429"));
|
||||
let schema = responses
|
||||
@@ -1065,7 +1074,7 @@ mod tests {
|
||||
Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
assert!(
|
||||
!responses.contains_key("418"),
|
||||
@@ -1080,7 +1089,7 @@ mod tests {
|
||||
&mut registry,
|
||||
external_spec("svc/op", vec![error("HTTP_500", Some(500))]),
|
||||
);
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_CALL, "post");
|
||||
let schema = responses
|
||||
.get("500")
|
||||
@@ -1098,7 +1107,7 @@ mod tests {
|
||||
#[test]
|
||||
fn doc_validates_against_openapiv3_parsing() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry);
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let text = serde_json::to_string(&spec.raw).unwrap();
|
||||
let parsed: openapiv3::OpenAPI =
|
||||
serde_json::from_str(&text).expect("gateway doc parses as OpenAPI 3.0");
|
||||
|
||||
Reference in New Issue
Block a user