use axum::middleware::from_fn_with_state;
use axum::routing::{get, post};
use axum::Router;
use super::middleware::{
bearer_auth, cors_layer, fallback, request_cancellation_layer, request_id_layer,
};
use super::state::AppState;
use super::{control, handlers};
pub fn build_router(state: AppState) -> Router {
let cors = cors_layer(&state.config.cors_allowed_origins);
Router::new()
.route("/health", get(handlers::health))
.route("/readyz", get(handlers::readyz))
.route("/metrics", get(handlers::metrics))
.route("/hf2q/v1/runtime", get(control::hf2q_runtime))
.route("/hf2q/v1/models/catalog", get(control::hub_gguf_catalog))
.route(
"/hf2q/v1/models/local-artifacts",
get(control::local_gguf_catalog),
)
.route("/hf2q/v1/models/activate", post(control::activate_model))
.route("/v1/models", get(handlers::list_models))
.route("/v1/models/:model_id", get(handlers::get_model))
.route("/v1/chat/completions", post(handlers::chat_completions))
.route("/v1/embeddings", post(handlers::embeddings))
.route("/shutdown", post(handlers::shutdown))
.fallback(fallback)
.layer(from_fn_with_state(state.clone(), bearer_auth))
.layer(axum::middleware::from_fn(request_cancellation_layer))
.layer(axum::middleware::from_fn(request_id_layer))
.layer(cors)
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::{to_bytes, Body};
use axum::http::{header, Request, StatusCode};
use tower::ServiceExt;
use super::super::state::ServerConfig;
fn state_default() -> AppState {
AppState::new(ServerConfig::default())
}
fn state_with_auth(token: &str) -> AppState {
let cfg = ServerConfig {
auth_token: Some(token.to_string()),
..Default::default()
};
AppState::new(cfg)
}
async fn body_string(resp: axum::response::Response) -> String {
let bytes = to_bytes(resp.into_body(), 1 << 20).await.unwrap();
String::from_utf8_lossy(&bytes).into_owned()
}
#[tokio::test]
async fn health_returns_200_with_json() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["status"], "ok");
assert_eq!(v["backend"], "mlx-native");
assert!(v["uptime_seconds"].as_u64().is_some());
}
#[tokio::test]
async fn readyz_returns_200_when_ready() {
let state = state_default();
state.mark_ready_for_gen();
let app = build_router(state);
let req = Request::builder()
.uri("/readyz")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["ready"], true);
}
#[tokio::test]
async fn readyz_returns_503_when_not_ready_with_retry_after() {
let state = state_default();
state.mark_not_ready();
let app = build_router(state);
let req = Request::builder()
.uri("/readyz")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
resp.headers()
.get(header::RETRY_AFTER)
.and_then(|v| v.to_str().ok()),
Some("1")
);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["ready"], false);
}
#[tokio::test]
async fn list_models_returns_empty_list_when_cache_unset() {
let cfg = ServerConfig {
cache_dir: None,
..Default::default()
};
let state = AppState::new(cfg);
let app = build_router(state);
let req = Request::builder()
.uri("/v1/models")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["object"], "list");
assert!(v["data"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn get_model_returns_404_when_absent() {
let cfg = ServerConfig {
cache_dir: None,
..Default::default()
};
let state = AppState::new(cfg);
let app = build_router(state);
let req = Request::builder()
.uri("/v1/models/does-not-exist")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["error"]["code"], "model_not_found");
assert_eq!(v["error"]["type"], "invalid_request_error");
assert!(v["error"]["message"]
.as_str()
.unwrap()
.contains("does-not-exist"));
}
#[tokio::test]
async fn unknown_route_returns_openai_shaped_404() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/this-route-does-not-exist")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["error"]["type"], "invalid_request_error");
assert!(v["error"]["message"]
.as_str()
.unwrap()
.contains("/this-route-does-not-exist"));
}
#[tokio::test]
async fn no_auth_configured_allows_unauthenticated_requests() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn auth_configured_rejects_missing_header_with_401() {
let app = build_router(state_with_auth("secret-token"));
let req = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["error"]["type"], "authentication_error");
}
#[tokio::test]
async fn auth_configured_rejects_wrong_token_with_401() {
let app = build_router(state_with_auth("secret-token"));
let req = Request::builder()
.uri("/health")
.header(header::AUTHORIZATION, "Bearer wrong-token")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn auth_configured_rejects_non_bearer_scheme_with_401() {
let app = build_router(state_with_auth("secret-token"));
let req = Request::builder()
.uri("/health")
.header(header::AUTHORIZATION, "Basic dXNlcjpwYXNz")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn auth_configured_accepts_correct_token() {
let app = build_router(state_with_auth("secret-token"));
let req = Request::builder()
.uri("/health")
.header(header::AUTHORIZATION, "Bearer secret-token")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn response_always_has_request_id_header() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let id = resp
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(!id.is_empty());
assert_eq!(id.len(), 36);
}
#[tokio::test]
async fn client_request_id_is_echoed_in_response() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/health")
.header("x-request-id", "client-supplied-id-42")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(
resp.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok()),
Some("client-supplied-id-42")
);
}
#[tokio::test]
async fn request_id_present_even_on_401() {
let app = build_router(state_with_auth("secret"));
let req = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let id = resp
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(!id.is_empty(), "request-id must be present on 401 too");
}
#[tokio::test]
async fn chat_completions_without_engine_returns_model_not_loaded() {
let app = build_router(state_default());
let body = r#"{"model":"gemma4","messages":[{"role":"user","content":"hi"}]}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body_text = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body_text).unwrap();
assert_eq!(v["error"]["code"], "model_not_loaded");
}
#[tokio::test]
async fn chat_completions_rejects_empty_messages() {
let app = build_router(state_default());
let body = r#"{"model":"gemma4","messages":[]}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert_eq!(v["error"]["type"], "invalid_request_error");
assert_eq!(v["error"]["code"], serde_json::Value::Null);
assert_eq!(v["error"]["param"], "messages");
assert_eq!(
v["error"]["message"],
"messages must contain at least one entry"
);
}
#[tokio::test]
async fn metrics_returns_prometheus_text_format() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(
ct.starts_with("text/plain") && ct.contains("version=0.0.4"),
"unexpected content-type: {}",
ct
);
let body = body_string(resp).await;
for expected in [
"hf2q_uptime_seconds",
"hf2q_ready",
"hf2q_model_loaded",
"hf2q_requests_total",
"hf2q_chat_completions_started",
"hf2q_decode_tokens_total",
"# HELP",
"# TYPE",
] {
assert!(
body.contains(expected),
"metric body missing {:?}\nbody:\n{}",
expected,
body
);
}
}
#[tokio::test]
async fn metrics_ready_gauge_reflects_state() {
let state = state_default();
state.mark_not_ready();
let app = build_router(state);
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = body_string(resp).await;
assert!(
body.lines().any(|l| l.trim() == "hf2q_ready 0"),
"expected `hf2q_ready 0` line; body:\n{}",
body
);
}
#[tokio::test]
async fn metrics_counter_increments_after_health_request() {
let state = state_default();
let app = build_router(state.clone());
let req = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let _ = app.oneshot(req).await.unwrap();
let app2 = build_router(state);
let req2 = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app2.oneshot(req2).await.unwrap();
let body = body_string(resp).await;
let line = body
.lines()
.find(|l| l.starts_with("hf2q_requests_total"))
.expect("requests_total line present");
let n: u64 = line
.split_whitespace()
.last()
.and_then(|s| s.parse().ok())
.expect("parse counter");
assert!(n >= 1, "requests_total should have been bumped, got {}", n);
}
#[tokio::test]
async fn bad_json_schema_returns_grammar_error() {
let app = build_router(state_default());
let body = r#"{
"model":"nope",
"messages":[{"role":"user","content":"hi"}],
"response_format":{"type":"json_schema","json_schema":{"name":"bad","schema":{"type":"not_a_real_type"}}}
}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert_eq!(v["error"]["code"], "model_not_loaded");
}
#[tokio::test]
async fn chat_completions_without_engine_multimodal_gate_is_secondary() {
let app = build_router(state_default());
let body = r#"{
"model":"nope",
"messages":[{"role":"user","content":[
{"type":"text","text":"what is this"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0K"}}
]}]
}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert!(
v["error"]["code"] == "model_not_loaded",
"engine-gate should win: got {:?}",
v
);
}
#[tokio::test]
async fn chat_completions_stream_without_engine_returns_model_not_loaded() {
let app = build_router(state_default());
let body = r#"{"model":"nope","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert_eq!(v["error"]["code"], "model_not_loaded");
}
#[tokio::test]
async fn chat_completions_malformed_json_returns_400() {
let app = build_router(state_default());
let body = r#"{ not valid json"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status().as_u16() / 100, 4, "4xx expected");
}
#[tokio::test]
async fn embeddings_route_returns_400_when_no_embedding_model_loaded() {
let app = build_router(state_default());
let body = r#"{"model": "any", "input": "hello"}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/embeddings")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body_string(resp).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["error"]["type"], "invalid_request_error");
assert_eq!(v["error"]["code"], "model_not_loaded");
}
#[tokio::test]
async fn embeddings_route_rejects_malformed_json_with_4xx() {
let app = build_router(state_default());
let req = Request::builder()
.method("POST")
.uri("/v1/embeddings")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{ not valid"#))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status().as_u16() / 100, 4, "4xx expected");
}
#[tokio::test]
async fn unknown_route_with_auth_still_gets_401_before_404() {
let app = build_router(state_with_auth("secret"));
let req = Request::builder()
.uri("/does-not-exist")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn iter209_chat_with_empty_pool_returns_model_not_loaded_for_unresolvable_name() {
let app = build_router(state_default());
let body = r#"{"model":"not-a-repo-or-path","messages":[{"role":"user","content":"hi"}]}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert_eq!(v["error"]["code"], "model_not_loaded");
assert!(
v["error"]["message"]
.as_str()
.unwrap_or("")
.contains("not-a-repo-or-path"),
"error message should name the unresolvable model: {v}"
);
}
#[tokio::test]
async fn iter209_v1_models_empty_pool_reports_no_loaded_entries() {
let cfg = ServerConfig {
cache_dir: None,
..Default::default()
};
let state = AppState::new(cfg);
let app = build_router(state);
let req = Request::builder()
.uri("/v1/models")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
let data = v["data"].as_array().expect("data is array");
for entry in data {
let loaded = entry["loaded"].as_bool().unwrap_or(false);
assert!(
!loaded,
"no entry should be loaded with empty pool; got {entry}"
);
}
}
#[tokio::test]
async fn iter209_metrics_model_loaded_zero_with_empty_pool() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = body_string(resp).await;
assert!(
body.lines().any(|l| l.trim() == "hf2q_model_loaded 0"),
"expected `hf2q_model_loaded 0` line; body:\n{}",
body
);
}
#[tokio::test]
async fn iter210_metrics_emits_pool_gauges() {
let app = build_router(state_default());
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(
body.contains("# HELP hf2q_pool_loaded_models"),
"missing HELP line for hf2q_pool_loaded_models; body:\n{body}"
);
assert!(
body.contains("# TYPE hf2q_pool_loaded_models gauge"),
"missing TYPE line for hf2q_pool_loaded_models; body:\n{body}"
);
assert!(
body.lines()
.any(|l| l.trim() == "hf2q_pool_loaded_models 0"),
"expected `hf2q_pool_loaded_models 0` line; body:\n{body}"
);
assert!(
body.contains("# HELP hf2q_pool_resident_bytes"),
"missing HELP line for hf2q_pool_resident_bytes; body:\n{body}"
);
assert!(
body.contains("# TYPE hf2q_pool_resident_bytes gauge"),
"missing TYPE line for hf2q_pool_resident_bytes; body:\n{body}"
);
assert!(
body.lines()
.any(|l| l.trim() == "hf2q_pool_resident_bytes 0"),
"expected `hf2q_pool_resident_bytes 0` line; body:\n{body}"
);
assert!(
body.contains("# HELP hf2q_pool_memory_budget_bytes"),
"missing HELP line for hf2q_pool_memory_budget_bytes; body:\n{body}"
);
assert!(
body.contains("# TYPE hf2q_pool_memory_budget_bytes gauge"),
"missing TYPE line for hf2q_pool_memory_budget_bytes; body:\n{body}"
);
let budget_line = body
.lines()
.find(|l| l.starts_with("hf2q_pool_memory_budget_bytes "))
.unwrap_or_else(|| {
panic!("expected hf2q_pool_memory_budget_bytes line; body:\n{body}")
});
let budget_val: u64 = budget_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| panic!("could not parse u64 from {budget_line:?}; body:\n{body}"));
assert!(
budget_val > 0,
"expected positive memory_budget_bytes, got {budget_val}; body:\n{body}"
);
}
#[tokio::test]
async fn iter209_two_concurrent_unresolvable_requests_both_400() {
let state = state_default();
let app1 = build_router(state.clone());
let app2 = build_router(state.clone());
let body = r#"{"model":"unresolvable-name","messages":[{"role":"user","content":"hi"}]}"#;
let req1 = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let req2 = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let (resp1, resp2) = tokio::join!(app1.oneshot(req1), app2.oneshot(req2));
let resp1 = resp1.unwrap();
let resp2 = resp2.unwrap();
assert_eq!(resp1.status(), StatusCode::BAD_REQUEST);
assert_eq!(resp2.status(), StatusCode::BAD_REQUEST);
let v1: serde_json::Value = serde_json::from_str(&body_string(resp1).await).unwrap();
let v2: serde_json::Value = serde_json::from_str(&body_string(resp2).await).unwrap();
assert_eq!(v1["error"]["code"], "model_not_loaded");
assert_eq!(v2["error"]["code"], "model_not_loaded");
let app3 = build_router(state);
let req3 = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp3 = app3.oneshot(req3).await.unwrap();
assert_eq!(resp3.status(), StatusCode::BAD_REQUEST);
let v3: serde_json::Value = serde_json::from_str(&body_string(resp3).await).unwrap();
assert_eq!(v3["error"]["code"], "model_not_loaded");
}
#[tokio::test]
async fn iter209_default_model_fallback_when_req_model_empty() {
let app = build_router(state_default());
let body = r#"{"model":"","messages":[{"role":"user","content":"hi"}]}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert_eq!(v["error"]["code"], "model_not_loaded");
}
use crate::serve::api::state::KV_SPILL_OUTCOMES;
use crate::serve::multi_model::{
RestoreErrorKind, RestoreOutcome, SpillErrorKind, SpillOutcome,
};
use crate::serve::quant_select::QuantType;
#[tokio::test]
async fn iter213_metrics_emits_kv_counters() {
let state = state_default();
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::EnqueuedBlocks(1),
);
state.kv_spill_counters.record_restore(
"acme/m1",
QuantType::Q4_K_M,
RestoreOutcome::RestoredBlocks(1),
);
let app = build_router(state);
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(
body.contains("# HELP hf2q_pool_kv_spills_total"),
"missing HELP for hf2q_pool_kv_spills_total; body:\n{body}"
);
assert!(
body.contains("# TYPE hf2q_pool_kv_spills_total counter"),
"missing TYPE for hf2q_pool_kv_spills_total; body:\n{body}"
);
assert!(
body.contains("# HELP hf2q_pool_kv_restores_total"),
"missing HELP for hf2q_pool_kv_restores_total; body:\n{body}"
);
assert!(
body.contains("# TYPE hf2q_pool_kv_restores_total counter"),
"missing TYPE for hf2q_pool_kv_restores_total; body:\n{body}"
);
for (outcome, expected) in [
("success", 1u64),
("codec_err", 0),
("io_err", 0),
("parity_fail", 0),
] {
let needle = format!(
"hf2q_pool_kv_spills_total{{repo=\"acme/m1\",quant=\"Q4_K_M\",outcome=\"{outcome}\"}} {expected}",
);
assert!(
body.contains(&needle),
"missing spill line {needle:?} in body:\n{body}"
);
}
for (outcome, expected) in [
("success", 1u64),
("codec_err", 0),
("io_err", 0),
("parity_fail", 0),
] {
let needle = format!(
"hf2q_pool_kv_restores_total{{repo=\"acme/m1\",quant=\"Q4_K_M\",outcome=\"{outcome}\"}} {expected}",
);
assert!(
body.contains(&needle),
"missing restore line {needle:?} in body:\n{body}"
);
}
assert!(
body.lines()
.any(|l| l.trim() == "hf2q_pool_loaded_models 0"),
"iter-210 hf2q_pool_loaded_models 0 line regression; body:\n{body}"
);
}
#[tokio::test]
async fn iter213_kv_counter_delta_under_synthetic_spill() {
let state = state_default();
const N: u64 = 3;
for _ in 0..N {
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::EnqueuedBlocks(1),
);
}
let app = build_router(state);
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = body_string(resp).await;
let needle = format!(
"hf2q_pool_kv_spills_total{{repo=\"acme/m1\",quant=\"Q4_K_M\",outcome=\"success\"}} {N}",
);
assert!(
body.contains(&needle),
"expected success counter == N={N} after {N} synthetic spills; \
body:\n{body}"
);
let state2 = state_default();
state2.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::EnqueuedBlocks(7),
);
let app2 = build_router(state2);
let req2 = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp2 = app2.oneshot(req2).await.unwrap();
let body2 = body_string(resp2).await;
let needle_per_call =
"hf2q_pool_kv_spills_total{repo=\"acme/m1\",quant=\"Q4_K_M\",outcome=\"success\"} 1";
assert!(
body2.contains(needle_per_call),
"per-call NOT per-block: EnqueuedBlocks(7) must increment by 1 not 7; \
body:\n{body2}"
);
}
#[tokio::test]
async fn iter213_kv_counter_outcome_cardinality_registered() {
assert_eq!(
KV_SPILL_OUTCOMES.len(),
4,
"KV_SPILL_OUTCOMES cardinality is FIXED at 4; adding a 5th \
outcome requires a Phase 4 amendment per AC 5472 closed-enum \
contract"
);
assert_eq!(
KV_SPILL_OUTCOMES,
&["success", "codec_err", "io_err", "parity_fail"],
"KV_SPILL_OUTCOMES order is load-bearing — record_spill \
indexes by usize and /metrics emits in this exact order; \
reordering breaks scrape diffs and ADR-017 Phase C \
expectations"
);
}
#[tokio::test]
async fn iter213_server_timing_header_default_off() {
let state = state_default();
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::EnqueuedBlocks(1),
);
assert!(
!state.kv_spill_counters.server_timing_enabled(),
"Server-Timing toggle MUST default OFF in iter-213"
);
let app = build_router(state);
let body = r#"{"model":"unresolvable-name","messages":[{"role":"user","content":"hi"}]}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert!(
resp.headers().get("server-timing").is_none(),
"Server-Timing header MUST NOT appear in iter-213 default-OFF \
responses; saw: {:?}",
resp.headers().get("server-timing")
);
}
#[tokio::test]
async fn iter213_kv_counter_error_outcomes_increment() {
let state = state_default();
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::Error(SpillErrorKind::CodecErr),
);
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::Error(SpillErrorKind::IoErr),
);
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::Error(SpillErrorKind::ParityFail),
);
state.kv_spill_counters.record_restore(
"acme/m1",
QuantType::Q4_K_M,
RestoreOutcome::Error(RestoreErrorKind::CodecErr),
);
state.kv_spill_counters.record_restore(
"acme/m1",
QuantType::Q4_K_M,
RestoreOutcome::Error(RestoreErrorKind::IoErr),
);
state.kv_spill_counters.record_restore(
"acme/m1",
QuantType::Q4_K_M,
RestoreOutcome::Error(RestoreErrorKind::ParityFail),
);
let app = build_router(state);
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = body_string(resp).await;
for (outcome, expected) in [
("success", 0u64),
("codec_err", 1),
("io_err", 1),
("parity_fail", 1),
] {
let spill_needle = format!(
"hf2q_pool_kv_spills_total{{repo=\"acme/m1\",quant=\"Q4_K_M\",outcome=\"{outcome}\"}} {expected}",
);
assert!(
body.contains(&spill_needle),
"missing spill error line {spill_needle:?} in body:\n{body}"
);
let restore_needle = format!(
"hf2q_pool_kv_restores_total{{repo=\"acme/m1\",quant=\"Q4_K_M\",outcome=\"{outcome}\"}} {expected}",
);
assert!(
body.contains(&restore_needle),
"missing restore error line {restore_needle:?} in body:\n{body}"
);
}
}
#[tokio::test]
async fn iter213_skipped_outcome_does_not_increment() {
let state = state_default();
const N: usize = 5;
for _ in 0..N {
state.kv_spill_counters.record_spill(
"acme/m1",
QuantType::Q4_K_M,
SpillOutcome::Skipped,
);
state.kv_spill_counters.record_restore(
"acme/m1",
QuantType::Q4_K_M,
RestoreOutcome::Skipped,
);
}
let app = build_router(state);
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = body_string(resp).await;
assert!(
body.contains("# HELP hf2q_pool_kv_spills_total"),
"HELP must emit even when no observations occurred; body:\n{body}"
);
assert!(
body.contains("# TYPE hf2q_pool_kv_spills_total counter"),
"TYPE must emit even when no observations occurred; body:\n{body}"
);
let absent_needle = "hf2q_pool_kv_spills_total{repo=\"acme/m1\"";
assert!(
!body.contains(absent_needle),
"Skipped outcome MUST NOT lazy-init the (repo, quant) row; \
saw line containing {absent_needle:?} in body:\n{body}"
);
let absent_restore = "hf2q_pool_kv_restores_total{repo=\"acme/m1\"";
assert!(
!body.contains(absent_restore),
"Skipped restore outcome MUST NOT lazy-init the row; \
saw line containing {absent_restore:?} in body:\n{body}"
);
}
#[tokio::test]
async fn iter209_default_model_fallback_uses_default_when_req_model_empty() {
let cfg = ServerConfig::default();
let state = AppState::new(cfg).with_default_model(Some("still-not-resolvable".into()));
let app = build_router(state);
let body = r#"{"model":"","messages":[{"role":"user","content":"hi"}]}"#;
let req = Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_str(&body_string(resp).await).unwrap();
assert_eq!(v["error"]["code"], "model_not_loaded");
assert!(
v["error"]["message"]
.as_str()
.unwrap_or("")
.contains("still-not-resolvable"),
"expected default_model name in error: {v}"
);
}
fn single_capacity_state() -> (AppState, super::super::engine::Engine) {
single_capacity_state_with_config(ServerConfig::default())
}
fn single_capacity_state_with_config(
config: ServerConfig,
) -> (AppState, super::super::engine::Engine) {
use crate::serve::multi_model::{DefaultModelLoader, HotSwapManager, LoadedPool};
use crate::serve::quant_select::QuantType;
use std::sync::Arc;
let mut state = AppState::new(config);
let mut manager = HotSwapManager::new(
LoadedPool::with_capacity_and_budget(1, 800),
Arc::new(DefaultModelLoader),
);
let engine = super::super::engine::make_synthetic_engine_for_test(
super::super::engine::LoadedArch::Qwen35,
);
manager
.admit_for_test("resident/model", QuantType::Q4_K_M, 400, engine.clone())
.unwrap();
state.pool = Arc::new(std::sync::RwLock::new(manager));
(state, engine)
}
struct RuntimeWarmupSentinelLoader {
calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl crate::serve::multi_model::ModelLoader<super::super::engine::Engine>
for RuntimeWarmupSentinelLoader
{
fn load(
&self,
_path: &std::path::Path,
_config: &crate::serve::multi_model::EngineConfig,
) -> anyhow::Result<super::super::engine::Engine> {
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(async { tokio::task::yield_now().await });
Ok(super::super::engine::make_synthetic_engine_for_test(
super::super::engine::LoadedArch::Qwen35,
))
}
}
fn single_capacity_state_with_runtime_warmup_loader() -> (
AppState,
super::super::engine::Engine,
std::sync::Arc<std::sync::atomic::AtomicUsize>,
) {
use crate::serve::multi_model::{HotSwapManager, LoadedPool};
use crate::serve::quant_select::QuantType;
use std::sync::Arc;
let mut state = AppState::new(ServerConfig::default());
let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let mut manager = HotSwapManager::new(
LoadedPool::with_capacity_and_budget(1, 800),
Arc::new(RuntimeWarmupSentinelLoader {
calls: Arc::clone(&calls),
}),
);
let engine = super::super::engine::make_synthetic_engine_for_test(
super::super::engine::LoadedArch::Qwen35,
);
manager
.admit_for_test("resident/model", QuantType::Q4_K_M, 400, engine.clone())
.unwrap();
state.pool = Arc::new(std::sync::RwLock::new(manager));
(state, engine, calls)
}
fn activation_target(bytes: usize) -> tempfile::NamedTempFile {
use std::io::Write;
let mut file = tempfile::Builder::new().suffix(".gguf").tempfile().unwrap();
let key = b"general.file_type";
let mut gguf = Vec::new();
gguf.extend_from_slice(b"GGUF");
gguf.extend_from_slice(&3_u32.to_le_bytes());
gguf.extend_from_slice(&0_u64.to_le_bytes());
gguf.extend_from_slice(&1_u64.to_le_bytes());
gguf.extend_from_slice(&(key.len() as u64).to_le_bytes());
gguf.extend_from_slice(key);
gguf.extend_from_slice(&4_u32.to_le_bytes());
gguf.extend_from_slice(&15_u32.to_le_bytes());
gguf.resize(bytes.max(gguf.len()), 0);
file.write_all(&gguf).unwrap();
file
}
fn hosted_candidate(state: &AppState, bytes: u64) -> String {
let view = state
.artifact_catalog
.register_hosted(crate::input::hf_download::HubGgufCatalog {
schema_version: "hf2q.hub-gguf-catalog.v2".into(),
repository: "owner/hosted".into(),
revision: "a".repeat(40),
artifacts: vec![crate::input::hf_download::HubGgufArtifact {
repository: "owner/hosted".into(),
revision: "a".repeat(40),
filename: "model-q6_k.gguf".into(),
bytes,
sha256: "b".repeat(64),
quant_hint: Some("Q6_K".into()),
role: "text_model".into(),
selectable: true,
unavailable_reason: None,
}],
})
.unwrap();
view.candidates[0].candidate_id.clone().unwrap()
}
#[tokio::test]
async fn hf2q_runtime_and_activation_routes_use_existing_bearer_auth() {
let app = build_router(state_with_auth("diagnostic-secret"));
let missing = app
.clone()
.oneshot(
Request::builder()
.uri("/hf2q/v1/runtime")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(missing.status(), StatusCode::UNAUTHORIZED);
let catalog_missing = app
.clone()
.oneshot(
Request::builder()
.uri("/hf2q/v1/models/catalog?model=owner%2Fmodel")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(catalog_missing.status(), StatusCode::UNAUTHORIZED);
let local_catalog_missing = app
.clone()
.oneshot(
Request::builder()
.uri("/hf2q/v1/models/local-artifacts?model=owner%2Fmodel")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(local_catalog_missing.status(), StatusCode::UNAUTHORIZED);
let runtime = app
.clone()
.oneshot(
Request::builder()
.uri("/hf2q/v1/runtime")
.header(header::AUTHORIZATION, "Bearer diagnostic-secret")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(runtime.status(), StatusCode::OK);
let body: serde_json::Value = serde_json::from_str(&body_string(runtime).await).unwrap();
assert_eq!(body["schema_version"], "hf2q.runtime.v1");
assert_eq!(body["capabilities"]["non_evicting_load"], true);
assert_eq!(
body["capabilities"]["artifact_resolution"],
"hf2q.artifact-resolution.v2"
);
assert_eq!(
body["capabilities"]["local_artifact_resolution"],
"hf2q.local-artifact-resolution.v1"
);
assert_eq!(
body["capabilities"]["diagnostic_no_evict_header"]["name"],
"x-hf2q-diagnostic-no-evict"
);
assert_eq!(
body["capabilities"]["diagnostic_no_evict_header"]["value"],
"1"
);
let local_catalog = app
.clone()
.oneshot(
Request::builder()
.uri("/hf2q/v1/models/local-artifacts?model=owner%2Fmodel")
.header(header::AUTHORIZATION, "Bearer diagnostic-secret")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(local_catalog.status(), StatusCode::OK);
let local_body: serde_json::Value =
serde_json::from_str(&body_string(local_catalog).await).unwrap();
assert_eq!(
local_body["schema_version"],
"hf2q.local-artifact-resolution.v1"
);
assert!(local_body["candidates"].as_array().unwrap().is_empty());
let activation = app
.oneshot(
Request::builder()
.method("POST")
.uri("/hf2q/v1/models/activate")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"model":"x"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(activation.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn diagnostic_no_evict_chat_header_is_authenticated_and_fails_closed_on_conflict() {
use crate::serve::quant_select::QuantType;
let (state, engine) = single_capacity_state_with_config(ServerConfig {
auth_token: Some("diagnostic-secret".to_string()),
..Default::default()
});
let target = activation_target(500);
let app = build_router(state.clone());
let body = serde_json::json!({
"model": target.path().to_string_lossy(),
"messages": [{"role": "user", "content": "diagnose"}],
});
let unauthorized = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.header("x-hf2q-diagnostic-no-evict", "1")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
let conflict = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::AUTHORIZATION, "Bearer diagnostic-secret")
.header(header::CONTENT_TYPE, "application/json")
.header("x-hf2q-diagnostic-no-evict", "1")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(conflict.status(), StatusCode::CONFLICT);
let receipt: serde_json::Value =
serde_json::from_str(&body_string(conflict).await).unwrap();
assert_eq!(receipt["error"]["code"], "diagnostic_model_conflict");
let manager = state.pool.read().unwrap();
assert!(manager
.try_get("resident/model", QuantType::Q4_K_M)
.is_some());
assert_eq!(manager.pool_stats().loaded_count, 1);
drop(manager);
engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn ordinary_chat_request_preserves_adr005_auto_swap_resolution() {
let (state, engine) = single_capacity_state();
let target = activation_target(500);
let app = build_router(state.clone());
let body = serde_json::json!({
"model": target.path().to_string_lossy(),
"messages": [{"role": "user", "content": "ordinary client"}],
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let error: serde_json::Value = serde_json::from_str(&body_string(response).await).unwrap();
assert_eq!(error["error"]["code"], "generation_error");
engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn diagnostic_no_evict_header_rejects_values_other_than_one() {
let app = build_router(state_default());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(header::CONTENT_TYPE, "application/json")
.header("x-hf2q-diagnostic-no-evict", "true")
.body(Body::from(
r#"{"model":"unused","messages":[{"role":"user","content":"hi"}]}"#,
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let error: serde_json::Value = serde_json::from_str(&body_string(response).await).unwrap();
assert_eq!(error["error"]["param"], "x-hf2q-diagnostic-no-evict");
}
#[tokio::test]
async fn activation_conflict_returns_exact_revision_and_never_loads_candidate() {
use crate::serve::quant_select::QuantType;
let (state, engine) = single_capacity_state();
let target = activation_target(500);
let app = build_router(state.clone());
let body = serde_json::json!({
"model": target.path().to_string_lossy(),
"action": "load",
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/hf2q/v1/models/activate")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let receipt: serde_json::Value =
serde_json::from_str(&body_string(response).await).unwrap();
assert_eq!(receipt["status"], "conflict");
assert_eq!(receipt["requires_explicit_switch"], true);
assert_eq!(receipt["victims"].as_array().unwrap().len(), 1);
assert_eq!(receipt["victims"][0]["pool_key"], "resident/model@Q4_K_M");
assert!(receipt["pool_revision"].as_u64().is_some());
let manager = state.pool.read().unwrap();
assert!(manager
.try_get("resident/model", QuantType::Q4_K_M)
.is_some());
assert_eq!(manager.pool_stats().loaded_count, 1);
drop(manager);
engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn hosted_activation_conflict_returns_before_transfer_helper() {
use crate::serve::quant_select::QuantType;
let (state, engine) = single_capacity_state();
let candidate_id = hosted_candidate(&state, 500);
let app = build_router(state.clone());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/hf2q/v1/models/activate")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::json!({
"model":"owner/hosted",
"candidate_id":candidate_id,
"action":"load"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let manager = state.pool.read().unwrap();
assert!(manager
.try_get("resident/model", QuantType::Q4_K_M)
.is_some());
assert_eq!(manager.pool_stats().loaded_count, 1);
drop(manager);
engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn explicit_switch_rejects_stale_revision_before_shutdown_or_load() {
use crate::serve::quant_select::QuantType;
let (state, engine) = single_capacity_state();
let target = activation_target(500);
let victim = state.pool.read().unwrap().iter_loaded().next().unwrap();
let app = build_router(state.clone());
let body = serde_json::json!({
"model": target.path().to_string_lossy(),
"action": "switch",
"expected_revision": 0,
"victims": [{
"pool_key": victim.pool_key,
"quant": victim.quant,
"bytes_resident": victim.bytes_resident,
"generation": victim.generation,
}],
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/hf2q/v1/models/activate")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let receipt: serde_json::Value =
serde_json::from_str(&body_string(response).await).unwrap();
assert_eq!(receipt["code"], "stale_activation_plan");
assert!(engine.is_worker_healthy());
let manager = state.pool.read().unwrap();
assert!(manager
.try_get("resident/model", QuantType::Q4_K_M)
.is_some());
assert_eq!(manager.pool_stats().loaded_count, 1);
drop(manager);
engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn stale_hosted_switch_returns_before_transfer_helper() {
use crate::serve::quant_select::QuantType;
let (state, engine) = single_capacity_state();
let candidate_id = hosted_candidate(&state, 500);
let victim = state.pool.read().unwrap().iter_loaded().next().unwrap();
let app = build_router(state.clone());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/hf2q/v1/models/activate")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::json!({
"model":"owner/hosted",
"candidate_id":candidate_id,
"action":"switch",
"expected_revision":0,
"victims":[{
"pool_key":victim.pool_key,
"quant":victim.quant,
"bytes_resident":victim.bytes_resident,
"generation":victim.generation
}]
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let manager = state.pool.read().unwrap();
assert!(manager
.try_get("resident/model", QuantType::Q4_K_M)
.is_some());
assert_eq!(manager.pool_stats().loaded_count, 1);
drop(manager);
engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn explicit_switch_loads_off_runtime_and_returns_the_new_resident() {
use crate::serve::quant_select::QuantType;
use std::sync::atomic::Ordering;
let (state, _victim_engine, loader_calls) =
single_capacity_state_with_runtime_warmup_loader();
let target = activation_target(500);
let victim = state.pool.read().unwrap().iter_loaded().next().unwrap();
let expected_revision = state.pool.read().unwrap().pool_stats().revision;
let app = build_router(state.clone());
let body = serde_json::json!({
"model": target.path().to_string_lossy(),
"action": "switch",
"expected_revision": expected_revision,
"victims": [{
"pool_key": victim.pool_key,
"quant": victim.quant,
"bytes_resident": victim.bytes_resident,
"generation": victim.generation,
}],
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/hf2q/v1/models/activate")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let receipt: serde_json::Value =
serde_json::from_str(&body_string(response).await).unwrap();
assert_eq!(receipt["status"], "switched");
assert!(receipt["pool_revision"].as_u64().unwrap() > expected_revision);
assert_eq!(loader_calls.load(Ordering::SeqCst), 1);
let candidate_repo = receipt["candidate"]["repo"].as_str().unwrap();
let replacement = {
let manager = state.pool.read().unwrap();
assert!(manager
.try_get("resident/model", QuantType::Q4_K_M)
.is_none());
manager
.try_get(candidate_repo, QuantType::Q4_K_M)
.expect("replacement must be resident")
};
replacement.engine.shutdown().await.unwrap();
}
#[tokio::test]
async fn qwen35_metrics_endpoint_works_with_qwen35_loaded() {
use super::super::engine;
use crate::serve::quant_select::QuantType;
let state = AppState::new(ServerConfig::default());
{
let mut mgr = state.pool.write().expect("pool rwlock");
let engine = engine::make_synthetic_engine_for_test(engine::LoadedArch::Qwen35);
mgr.admit_for_test(
"iter-215-qwen35-test",
QuantType::Q4_K_M,
1024,
engine,
)
.expect("admit synthetic Qwen35 engine");
}
let app = build_router(state);
let req = Request::builder()
.uri("/metrics")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(
body.contains("hf2q_pool_loaded_models"),
"missing pool gauge: {body}"
);
assert!(
body.contains("hf2q_pool_resident_bytes"),
"missing pool gauge: {body}"
);
assert!(
body.contains("hf2q_pool_memory_budget_bytes"),
"missing pool gauge: {body}"
);
assert!(
body.contains("hf2q_pool_kv_spills_total"),
"missing kv_spills HELP/TYPE: {body}"
);
assert!(
body.contains("hf2q_pool_kv_restores_total"),
"missing kv_restores HELP/TYPE: {body}"
);
assert!(
body.lines()
.any(|l| l.trim() == "hf2q_pool_loaded_models 1"),
"expected `hf2q_pool_loaded_models 1` line; got body: {body}"
);
}
}