mod common; use std::time::Duration; use fabro_http::header::AUTHORIZATION; use serde_json::json; #[tokio::test] async fn scripted_budget_and_content_filter_errors_are_distinct() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "quota" }, "script": { "kind": "error", "status": 429, "message": "quota exceeded", "error_type": "insufficient_quota", "code": "quota_exceeded", "retry_after": "120" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "filter" }, "script": { "kind": "error", "status": 400, "message": "content filtered", "error_type": "content_filter", "code": "content_filter" } } ] })) .await; let quota = server .post_responses(json!({ "model": "gpt-test", "input": "quota please", "stream": false })) .await; assert_eq!(quota.status(), 429); assert_eq!(quota.headers()["retry-after"], "120"); let quota_body = quota.json::().await.expect("json"); assert_eq!(quota_body["error"]["type"], "insufficient_quota"); let filter = server .post_responses(json!({ "model": "gpt-test", "input": "filter please", "stream": false })) .await; assert_eq!(filter.status(), 400); let filter_body = filter.json::().await.expect("json"); assert_eq!(filter_body["error"]["type"], "content_filter"); } #[tokio::test] async fn scripted_error_status_matrix_is_preserved() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "400" }, "script": { "kind": "error", "status": 400, "message": "400 error", "error_type": "invalid_request_error", "code": "400_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "401" }, "script": { "kind": "error", "status": 401, "message": "401 error", "error_type": "invalid_api_key", "code": "401_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "403" }, "script": { "kind": "error", "status": 403, "message": "403 error", "error_type": "permission_error", "code": "403_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "404" }, "script": { "kind": "error", "status": 404, "message": "404 error", "error_type": "not_found_error", "code": "404_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "408" }, "script": { "kind": "error", "status": 408, "message": "408 error", "error_type": "timeout_error", "code": "408_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "413" }, "script": { "kind": "error", "status": 413, "message": "413 error", "error_type": "request_too_large", "code": "413_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "429 status" }, "script": { "kind": "error", "status": 429, "message": "429 error", "error_type": "rate_limit_error", "code": "429_code", "retry_after": "30" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "500" }, "script": { "kind": "error", "status": 500, "message": "500 error", "error_type": "server_error", "code": "500_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "502" }, "script": { "kind": "error", "status": 502, "message": "502 error", "error_type": "bad_gateway", "code": "502_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "503" }, "script": { "kind": "error", "status": 503, "message": "503 error", "error_type": "service_unavailable", "code": "503_code" } }, { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false, "input_contains": "504" }, "script": { "kind": "error", "status": 504, "message": "504 error", "error_type": "gateway_timeout", "code": "504_code" } } ] })) .await; for status in [400_u16, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504] { let marker = if status == 429 { "429 status".to_owned() } else { status.to_string() }; let response = server .post_responses(json!({ "model": "gpt-test", "input": marker, "stream": false })) .await; assert_eq!(response.status().as_u16(), status); let headers = response.headers().clone(); let body = response.json::().await.expect("json"); assert_eq!(body["error"]["message"], format!("{status} error")); if status == 429 { assert_eq!(headers["retry-after"], "30"); } } } #[tokio::test] async fn scripted_hang_times_out_client_side() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": false }, "script": { "kind": "hang" } } ] })) .await; let client = fabro_http::HttpClientBuilder::new() .no_proxy() .timeout(Duration::from_millis(150)) .default_headers( [( AUTHORIZATION, server .authorization_header_value() .parse() .expect("valid header"), )] .into_iter() .collect(), ) .build() .expect("client"); let result = client .post(format!("{}/v1/responses", server.base_url)) .json(&json!({ "model": "gpt-test", "input": "hang", "stream": false })) .send() .await; assert!(result.is_err()); assert!(result.expect_err("timeout").is_timeout()); } #[tokio::test] async fn scripted_partial_stream_then_close_is_observable() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": true }, "script": { "kind": "success", "response_text": "partial stream", "close_after_chunks": 2 } } ] })) .await; let raw = server .post_responses_stream_raw( json!({ "model": "gpt-test", "input": "partial", "stream": true }), ) .await; let parsed = common::parse_sse_transcript(&raw.body).expect("valid sse prefix"); let joined = String::from_utf8(raw.body).expect("utf8"); assert_eq!(raw.status, 200); assert_eq!( raw.headers .get("content-type") .expect("content-type header"), "text/event-stream" ); assert!(joined.contains("event: response.created")); assert!(!joined.contains("event: malformed")); assert!(!parsed.done); } #[tokio::test] async fn scripted_partial_stream_then_close_is_observable_for_chat() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "chat.completions", "model": "gpt-test", "stream": true }, "script": { "kind": "success", "response_text": "partial chat stream", "close_after_chunks": 2 } } ] })) .await; let raw = server .post_chat_stream_raw(json!({ "model": "gpt-test", "messages": [{ "role": "user", "content": "partial chat" }], "stream": true })) .await; let parsed = common::parse_sse_transcript(&raw.body).expect("valid sse prefix"); let joined = String::from_utf8(raw.body).expect("utf8"); assert_eq!(raw.status, 200); assert!(joined.contains("chat.completion.chunk")); assert!(!joined.contains("event: malformed")); assert!(!parsed.done); } #[tokio::test] async fn scripted_delayed_first_byte_is_observable() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": true }, "script": { "kind": "success", "response_text": "delayed stream", "delay_before_headers_ms": 120 } } ] })) .await; let timed = server .post_responses_stream_timed( json!({ "model": "gpt-test", "input": "delay", "stream": true }), ) .await; assert_eq!(timed.status, 200); assert!(timed.first_event_elapsed >= Duration::from_millis(100)); assert!(timed.chunks.join("").contains("delayed stream")); } #[tokio::test] async fn scripted_malformed_sse_is_observable() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "chat.completions", "model": "gpt-test", "stream": true }, "script": { "kind": "success", "response_text": "broken stream", "malformed_sse": true } } ] })) .await; let raw = server .post_chat_stream_raw(json!({ "model": "gpt-test", "messages": [{ "role": "user", "content": "malformed" }], "stream": true })) .await; let joined = String::from_utf8(raw.body.clone()).expect("utf8"); let parse_error = common::parse_sse_transcript(&raw.body).expect_err("malformed sse"); assert_eq!(raw.status, 200); assert!(joined.contains("event: malformed")); assert!(parse_error.contains("incomplete")); } #[tokio::test] async fn scripted_malformed_sse_is_observable_for_responses() { let server = common::spawn_server().await.expect("server should start"); server .enqueue_scenarios(json!({ "scenarios": [ { "matcher": { "endpoint": "responses", "model": "gpt-test", "stream": true }, "script": { "kind": "success", "response_text": "broken response stream", "malformed_sse": true } } ] })) .await; let raw = server .post_responses_stream_raw(json!({ "model": "gpt-test", "input": "malformed responses", "stream": true })) .await; let joined = String::from_utf8(raw.body.clone()).expect("utf8"); let parse_error = common::parse_sse_transcript(&raw.body).expect_err("malformed sse"); assert_eq!(raw.status, 200); assert!(joined.contains("event: malformed")); assert!(parse_error.contains("incomplete")); } #[tokio::test] async fn localhost_success_paths_stay_fast_enough() { let server = common::spawn_server().await.expect("server should start"); let started = std::time::Instant::now(); let responses = server .post_responses(json!({ "model": "gpt-test", "input": "speed", "stream": false })) .await; let responses_elapsed = started.elapsed(); assert_eq!(responses.status(), 200); assert!(responses_elapsed < Duration::from_secs(1)); let started = std::time::Instant::now(); let timed = server .post_responses_stream_timed( json!({ "model": "gpt-test", "input": "speed stream", "stream": true }), ) .await; let stream_elapsed = started.elapsed(); assert_eq!(timed.status, 200); assert!(timed.first_event_elapsed < Duration::from_secs(1)); assert!(stream_elapsed < Duration::from_secs(1)); assert!(!timed.chunks.is_empty()); let started = std::time::Instant::now(); let chat = server .post_chat(json!({ "model": "gpt-test", "messages": [{ "role": "user", "content": "speed chat" }], "stream": false })) .await; let chat_elapsed = started.elapsed(); assert_eq!(chat.status(), 200); assert!(chat_elapsed < Duration::from_secs(1)); }