#[tokio::test]
#[ignore = "APR audit integration test - depends on predict endpoint"]
async fn test_apr_audit_endpoint() {
let state = AppState::demo().expect("test");
let app = create_router(state);
let predict_request = PredictRequest {
model: None,
features: vec![1.0, 2.0, 3.0, 4.0],
feature_names: None,
top_k: None,
include_confidence: true,
};
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/predict")
.header("content-type", "application/json")
.body(Body::from(
serde_json::to_string(&predict_request).expect("test"),
))
.expect("test"),
)
.await
.expect("test");
assert!(
response.status() == StatusCode::OK
|| response.status() == StatusCode::NOT_FOUND
|| response.status() == StatusCode::INTERNAL_SERVER_ERROR
|| response.status() == StatusCode::SERVICE_UNAVAILABLE
|| response.status() == StatusCode::UNPROCESSABLE_ENTITY
);
if response.status() != StatusCode::OK {
return;
}
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("test");
let predict_result: PredictResponse = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(_) => return, };
let request_id = predict_result.request_id;
let audit_response = app
.oneshot(
Request::builder()
.uri(format!("/v1/audit/{}", request_id))
.body(Body::empty())
.expect("test"),
)
.await
.expect("test");
assert_eq!(audit_response.status(), StatusCode::OK);
let audit_body = axum::body::to_bytes(audit_response.into_body(), usize::MAX)
.await
.expect("test");
let audit_result: AuditResponse = match serde_json::from_slice(&audit_body) {
Ok(v) => v,
Err(_) => return, };
assert_eq!(audit_result.record.request_id, request_id);
}
#[tokio::test]
async fn test_apr_audit_invalid_id() {
let app = create_test_app_shared();
let response = app
.oneshot(
Request::builder()
.uri("/v1/audit/not-a-valid-uuid")
.body(Body::empty())
.expect("test"),
)
.await
.expect("test");
assert!(
response.status() == StatusCode::BAD_REQUEST
|| response.status() == StatusCode::NOT_FOUND
|| response.status() == StatusCode::INTERNAL_SERVER_ERROR
|| response.status() == StatusCode::SERVICE_UNAVAILABLE
|| response.status() == StatusCode::UNPROCESSABLE_ENTITY
);
}
#[test]
fn test_predict_request_serialization() {
let request = PredictRequest {
model: Some("test-model".to_string()),
features: vec![1.0, 2.0, 3.0],
feature_names: Some(vec!["f1".to_string(), "f2".to_string(), "f3".to_string()]),
top_k: Some(3),
include_confidence: true,
};
let json = serde_json::to_string(&request).expect("test");
assert!(json.contains("test-model"));
assert!(json.contains("features"));
let deserialized: PredictRequest = serde_json::from_str(&json).expect("test");
assert_eq!(deserialized.features.len(), 3);
}
#[test]
fn test_explain_request_defaults() {
let json = r#"{"features": [1.0], "feature_names": ["f1"]}"#;
let request: ExplainRequest = serde_json::from_str(json).expect("test");
assert_eq!(request.top_k_features, 5); assert_eq!(request.method, "shap"); }
#[test]
#[cfg(feature = "gpu")]
fn test_imp_084_app_state_with_gpu_model() {
use crate::gpu::{GpuModel, GpuModelConfig};
let config = GpuModelConfig {
vocab_size: 256,
hidden_dim: 64,
num_heads: 2,
num_kv_heads: 2, num_layers: 2,
intermediate_dim: 128,
eps: 1e-5,
rope_theta: 10000.0,
explicit_head_dim: None,
layer_types: None,
linear_key_head_dim: None,
linear_value_head_dim: None,
linear_num_key_heads: None,
linear_num_value_heads: None,
linear_conv_kernel_dim: None,
constraints: None,
num_experts: None,
num_experts_per_tok: None,
expert_intermediate_size: None,
};
let gpu_model = GpuModel::new(config).expect("Failed to create GPU model");
let state = AppState::with_gpu_model(gpu_model).expect("Failed to create AppState");
assert!(
state.has_gpu_model(),
"IMP-084: AppState should have GPU model"
);
}
#[tokio::test]
#[cfg(feature = "gpu")]
async fn test_imp_085_completions_uses_gpu_model() {
use crate::gpu::{GpuModel, GpuModelConfig};
let config = GpuModelConfig {
vocab_size: 256,
hidden_dim: 64,
num_heads: 2,
num_kv_heads: 2, num_layers: 2,
intermediate_dim: 128,
eps: 1e-5,
rope_theta: 10000.0,
explicit_head_dim: None,
layer_types: None,
linear_key_head_dim: None,
linear_value_head_dim: None,
linear_num_key_heads: None,
linear_num_value_heads: None,
linear_conv_kernel_dim: None,
constraints: None,
num_experts: None,
num_experts_per_tok: None,
expert_intermediate_size: None,
};
let gpu_model = GpuModel::new(config).expect("Failed to create GPU model");
let state = AppState::with_gpu_model(gpu_model).expect("Failed to create AppState");
let app = create_router(state);
let request = CompletionRequest {
stream: false,
n: crate::api::ChoiceCount::ONE,
prompt: "Hello".to_string(),
max_tokens: Some(5),
temperature: Some(0.0),
model: "default".to_string(),
top_p: None,
stop: None,
};
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/completions")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&request).expect("test")))
.expect("test"),
)
.await
.expect("test");
assert_eq!(
response.status(),
StatusCode::OK,
"IMP-085: /v1/completions should work with GPU model"
);
}
#[test]
#[cfg(feature = "gpu")]
fn test_imp_116a_appstate_cached_model_storage() {
use crate::gguf::{GGUFConfig, OwnedQuantizedModelCachedSync};
let config = GGUFConfig {
architecture: "test".to_string(),
constraints: crate::gguf::ArchConstraints::from_architecture("test"),
hidden_dim: 64,
intermediate_dim: 128,
num_layers: 1,
num_heads: 4,
num_kv_heads: 4,
vocab_size: 100,
context_length: 128,
rope_theta: 10000.0,
eps: 1e-5,
rope_type: 0,
explicit_head_dim: None,
query_pre_attn_scalar: None,
bos_token_id: None,
eos_token_id: None,
};
let model = create_test_quantized_model(&config);
let cached_model = OwnedQuantizedModelCachedSync::new(model);
let state = AppState::with_cached_model(cached_model)
.expect("IMP-116a: AppState should accept cached model");
assert!(
state.cached_model().is_some(),
"IMP-116a: Cached model should be accessible from AppState"
);
}
#[tokio::test]
#[cfg(feature = "gpu")]
async fn test_imp_116b_cached_model_thread_safety() {
use crate::gguf::{GGUFConfig, OwnedQuantizedModelCachedSync};
use std::sync::Arc;
let config = GGUFConfig {
architecture: "test".to_string(),
constraints: crate::gguf::ArchConstraints::from_architecture("test"),
hidden_dim: 64,
intermediate_dim: 128,
num_layers: 1,
num_heads: 4,
num_kv_heads: 4,
vocab_size: 100,
context_length: 128,
rope_theta: 10000.0,
eps: 1e-5,
rope_type: 0,
explicit_head_dim: None,
query_pre_attn_scalar: None,
bos_token_id: None,
eos_token_id: None,
};
let model = create_test_quantized_model(&config);
let cached_model = Arc::new(OwnedQuantizedModelCachedSync::new(model));
let mut handles = Vec::new();
for i in 0..4 {
let model_clone = cached_model.clone();
handles.push(tokio::spawn(async move {
let inner = model_clone.model();
assert_eq!(inner.config.hidden_dim, 64, "Task {i} should access model");
}));
}
for handle in handles {
handle
.await
.expect("IMP-116b: Concurrent access should succeed");
}
}
#[tokio::test]
#[cfg(feature = "gpu")]
async fn test_imp_116c_completions_uses_cached_model() {
use crate::gguf::{GGUFConfig, OwnedQuantizedModelCachedSync};
let config = GGUFConfig {
architecture: "test".to_string(),
constraints: crate::gguf::ArchConstraints::from_architecture("test"),
hidden_dim: 64,
intermediate_dim: 128,
num_layers: 1,
num_heads: 4,
num_kv_heads: 4,
vocab_size: 100,
context_length: 128,
rope_theta: 10000.0,
eps: 1e-5,
rope_type: 0,
explicit_head_dim: None,
query_pre_attn_scalar: None,
bos_token_id: None,
eos_token_id: None,
};
let model = create_test_quantized_model(&config);
let cached_model = OwnedQuantizedModelCachedSync::new(model);
let state = AppState::with_cached_model(cached_model).expect("Failed to create AppState");
assert!(
state.has_cached_model(),
"IMP-116c: AppState should have cached model"
);
assert!(
state.cached_model().is_some(),
"IMP-116c: cached_model() should return Some"
);
let app = create_router(state);
let request = CompletionRequest {
stream: false,
n: crate::api::ChoiceCount::ONE,
prompt: "Hello".to_string(),
max_tokens: Some(3),
temperature: Some(0.0),
model: "default".to_string(),
top_p: None,
stop: None,
};
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/v1/completions")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&request).expect("test")))
.expect("test"),
)
.await
.expect("test");
let status = response.status();
assert!(
status == StatusCode::OK || status == StatusCode::INTERNAL_SERVER_ERROR,
"IMP-116c: Request should be handled (got {})",
status
);
}