use studio_worker::http::ApiClient;
use studio_worker::test_support::capture as captured_logs_for;
use studio_worker::types::*;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn caps() -> WorkerCapabilities {
WorkerCapabilities {
machine_name: "t".into(),
username: "u".into(),
agent_version: "0".into(),
engine: "synthetic".into(),
vram_total_gb: 0.0,
vram_threshold_gb: 0.0,
auto_enabled: true,
auto_start: false,
supported_models: vec![],
task_kinds: vec![],
supported_models_per_kind: Default::default(),
}
}
fn detached<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
std::thread::spawn(f)
.join()
.expect("worker thread panicked")
}
#[tokio::test]
async fn complete_surfaces_4xx() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/w/jobs/j/complete"))
.respond_with(ResponseTemplate::new(409).set_body_string("conflict"))
.mount(&server)
.await;
let uri = server.uri();
let err = detached(move || {
let api = ApiClient::new(uri).unwrap();
api.complete("w", "t", "j", "webp", "p", vec![1, 2, 3])
.unwrap_err()
});
assert!(err.to_string().contains("complete failed"));
}
#[tokio::test]
async fn complete_for_mp3_uses_audio_mime() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/w/jobs/j/complete"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })))
.mount(&server)
.await;
let uri = server.uri();
detached(move || {
let api = ApiClient::new(uri).unwrap();
api.complete("w", "t", "j", "mp3", "p", vec![1, 2, 3])
.unwrap();
});
}
#[tokio::test]
async fn complete_for_mp4_uses_video_mime() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/w/jobs/j/complete"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })))
.mount(&server)
.await;
let uri = server.uri();
detached(move || {
let api = ApiClient::new(uri).unwrap();
api.complete("w", "t", "j", "mp4", "p", vec![1, 2, 3])
.unwrap();
});
}
#[tokio::test]
async fn complete_for_unknown_ext_falls_back_to_octet_stream() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/w/jobs/j/complete"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })))
.mount(&server)
.await;
let uri = server.uri();
detached(move || {
let api = ApiClient::new(uri).unwrap();
api.complete("w", "t", "j", "bin", "p", vec![1, 2, 3])
.unwrap();
});
}
#[tokio::test]
async fn complete_logs_upload_byte_size() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/w/jobs/j/complete"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })))
.mount(&server)
.await;
let uri = server.uri();
let logs = captured_logs_for(move || {
let api = ApiClient::new(uri).unwrap();
api.complete("w", "t", "j", "webp", "p", vec![1, 2, 3, 4, 5])
.unwrap();
});
assert!(
logs.contains("op=\"complete\""),
"expected complete op field: {logs}"
);
assert!(
logs.contains("bytes=5"),
"expected upload byte-size field, got: {logs}"
);
}
#[tokio::test]
async fn complete_logs_upload_byte_size_even_on_failure() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/w/jobs/j/complete"))
.respond_with(ResponseTemplate::new(500).set_body_string("boom"))
.mount(&server)
.await;
let uri = server.uri();
let logs = captured_logs_for(move || {
let api = ApiClient::new(uri).unwrap();
let _ = api.complete("w", "t", "j", "webp", "p", vec![1, 2, 3, 4, 5, 6, 7]);
});
assert!(
logs.contains("bytes=7"),
"expected attempted upload byte-size field on failure, got: {logs}"
);
}
#[tokio::test]
async fn successful_call_emits_debug_tracing_event() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/register-request"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"requestId": "rr-1",
"status": "pending",
})))
.mount(&server)
.await;
let uri = server.uri();
let logs = captured_logs_for(move || {
let api = ApiClient::new(uri).unwrap();
api.register_request(&AutoRegisterRequest {
install_id: "id".into(),
registration_secret_hash: "h".into(),
capabilities: caps(),
user_agent: "studio-worker/test".into(),
})
.unwrap();
});
assert!(logs.contains("DEBUG"), "expected DEBUG event, got: {logs}");
assert!(
logs.contains("/graphics/api/workers/register-request"),
"expected endpoint in log, got: {logs}"
);
assert!(
logs.contains("op=\"register-request\""),
"expected op field: {logs}"
);
assert!(logs.contains("status=200"), "expected status field: {logs}");
assert!(logs.contains("elapsed_ms"), "expected elapsed_ms: {logs}");
}
#[tokio::test]
async fn failing_call_emits_warn_tracing_event_with_body() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/graphics/api/workers/register-request"))
.respond_with(ResponseTemplate::new(503).set_body_string("upstream-unavailable"))
.mount(&server)
.await;
let uri = server.uri();
let logs = captured_logs_for(move || {
let api = ApiClient::new(uri).unwrap();
let _ = api.register_request(&AutoRegisterRequest {
install_id: "id".into(),
registration_secret_hash: "h".into(),
capabilities: caps(),
user_agent: "studio-worker/test".into(),
});
});
assert!(logs.contains("WARN"), "expected WARN event, got: {logs}");
assert!(
logs.contains("op=\"register-request\""),
"expected op field: {logs}"
);
assert!(logs.contains("status=503"), "expected status field: {logs}");
assert!(
logs.contains("upstream-unavailable"),
"expected response body in log, got: {logs}"
);
}