use std::io::Cursor;
use studio_worker::config::Config;
use studio_worker::engine;
use studio_worker::types::*;
fn synth_engine() -> Box<dyn engine::Engine> {
let cfg = Config::default();
engine::build(&cfg).expect("synthetic engine should build")
}
#[test]
fn dispatch_image_round_trips_through_webp_decoder() {
let task = Task::Image(ImageParams {
prompt: "a stone golem".into(),
width: 512,
height: 512,
steps: 20,
ext: "webp".into(),
..Default::default()
});
let res = synth_engine().dispatch("synthetic", task).unwrap();
let (bytes, ext) = match res {
TaskResult::Image { bytes, ext } => (bytes, ext),
_ => panic!("expected image"),
};
assert_eq!(ext, "webp");
let img = image::ImageReader::new(Cursor::new(&bytes))
.with_guessed_format()
.unwrap()
.decode()
.expect("real WebP");
assert_eq!(img.width(), 512);
assert_eq!(img.height(), 512);
}
#[test]
fn dispatch_llm_returns_chat_completion_shape() {
let task = Task::Llm(LlmParams {
messages: vec![ChatMessage {
role: "user".into(),
content: "two plus two".into(),
}],
max_tokens: 32,
temperature: 0.5,
..Default::default()
});
let res = synth_engine().dispatch("synthetic-llm", task).unwrap();
let json = match res {
TaskResult::Llm { json } => json,
_ => panic!("expected llm"),
};
assert_eq!(json["object"], "chat.completion");
let content = json["choices"][0]["message"]["content"].as_str().unwrap();
assert!(content.starts_with("[synthetic]"));
}
#[test]
fn dispatch_stt_returns_whisper_shape() {
let task = Task::AudioStt(AudioSttParams {
input_url: "https://example.com/clip.wav".into(),
language: Some("nl".into()),
..Default::default()
});
let res = synth_engine().dispatch("synthetic-stt", task).unwrap();
let json = match res {
TaskResult::AudioStt { json } => json,
_ => panic!("expected stt"),
};
assert_eq!(json["language"], "nl");
assert!(json["text"].as_str().unwrap().starts_with("[synthetic]"));
}
#[test]
fn dispatch_tts_round_trips_through_wav_decoder() {
let task = Task::AudioTts(AudioTtsParams {
text: "hello cruel world".into(),
voice: "default".into(),
ext: "wav".into(),
..Default::default()
});
let res = synth_engine().dispatch("synthetic-tts", task).unwrap();
let (bytes, ext) = match res {
TaskResult::AudioTts { bytes, ext } => (bytes, ext),
_ => panic!("expected tts"),
};
assert_eq!(ext, "wav");
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
let reader = hound::WavReader::new(Cursor::new(bytes)).expect("real WAV");
let spec = reader.spec();
assert_eq!(spec.sample_rate, 22_050);
assert_eq!(spec.channels, 1);
assert_eq!(spec.bits_per_sample, 16);
}
#[test]
fn dispatch_video_emits_decodable_bytes() {
let task = Task::Video(VideoParams {
prompt: "a tiny dragon flapping wings".into(),
seconds: 2.0,
width: 256,
height: 256,
ext: "mp4".into(),
..Default::default()
});
let res = synth_engine().dispatch("synthetic-video", task).unwrap();
let (bytes, ext) = match res {
TaskResult::Video { bytes, ext } => (bytes, ext),
_ => panic!("expected video"),
};
assert_eq!(ext, "webp");
let img = image::ImageReader::new(Cursor::new(&bytes))
.with_guessed_format()
.unwrap()
.decode()
.expect("real WebP");
assert!(img.width() > 0);
}
#[test]
fn capabilities_flat_models_returns_all_kinds_flattened() {
let engine = synth_engine();
let caps = engine.capabilities();
let flat = caps.flat_models();
assert!(!flat.is_empty());
assert!(flat.iter().any(|m| m == "synthetic"));
}
#[test]
fn capabilities_kinds_lists_each_advertised_kind() {
let engine = synth_engine();
let kinds = engine.capabilities().kinds();
assert_eq!(kinds.len(), TaskKind::ALL.len());
}
#[test]
fn capabilities_supports_returns_false_for_unknown_model() {
let engine = synth_engine();
let caps = engine.capabilities();
assert!(!caps.supports(TaskKind::Image, "definitely-not-a-model"));
}
#[test]
fn capabilities_supports_returns_false_for_unsupported_model() {
let engine = synth_engine();
let caps = engine.capabilities();
assert!(!caps.supports(TaskKind::Llm, "definitely-not-a-model"));
}
#[test]
fn build_returns_a_multi_engine_named_multi() {
let engine = synth_engine();
assert_eq!(engine.name(), "multi");
}
#[test]
fn capabilities_advertise_every_kind() {
let engine = synth_engine();
let caps = engine.capabilities();
for kind in TaskKind::ALL {
assert!(
caps.supported_models_per_kind.contains_key(&kind),
"{} advertised",
kind.as_str()
);
}
}
#[test]
fn task_kind_matches_for_every_variant() {
assert_eq!(
Task::Image(ImageParams {
prompt: "x".into(),
width: 1,
height: 1,
steps: 1,
ext: "webp".into(),
..Default::default()
})
.kind(),
TaskKind::Image
);
assert_eq!(
Task::Llm(LlmParams {
messages: vec![],
max_tokens: 1,
temperature: 0.0,
..Default::default()
})
.kind(),
TaskKind::Llm
);
assert_eq!(
Task::AudioStt(AudioSttParams {
input_url: "http://x".into(),
language: None,
..Default::default()
})
.kind(),
TaskKind::AudioStt
);
assert_eq!(
Task::AudioTts(AudioTtsParams {
text: "x".into(),
voice: "v".into(),
ext: "wav".into(),
..Default::default()
})
.kind(),
TaskKind::AudioTts
);
assert_eq!(
Task::Video(VideoParams {
prompt: "x".into(),
seconds: 1.0,
width: 256,
height: 256,
ext: "mp4".into(),
..Default::default()
})
.kind(),
TaskKind::Video
);
}
#[test]
fn task_result_kind_matches_for_every_variant() {
let cases = [
(
TaskResult::Image {
bytes: vec![1],
ext: "webp".into(),
},
TaskKind::Image,
),
(
TaskResult::Llm {
json: serde_json::json!({}),
},
TaskKind::Llm,
),
(
TaskResult::AudioStt {
json: serde_json::json!({}),
},
TaskKind::AudioStt,
),
(
TaskResult::AudioTts {
bytes: vec![1],
ext: "wav".into(),
},
TaskKind::AudioTts,
),
(
TaskResult::Video {
bytes: vec![1],
ext: "mp4".into(),
},
TaskKind::Video,
),
];
for (r, expected) in cases {
assert_eq!(r.kind(), expected);
}
}
#[test]
fn task_kind_as_str_round_trips_with_serde() {
assert_eq!(TaskKind::Image.as_str(), "image");
assert_eq!(TaskKind::Llm.as_str(), "llm");
assert_eq!(TaskKind::AudioStt.as_str(), "audio_stt");
assert_eq!(TaskKind::AudioTts.as_str(), "audio_tts");
assert_eq!(TaskKind::Video.as_str(), "video");
}
#[test]
fn image_params_uses_serde_defaults_for_missing_fields() {
let json = serde_json::json!({ "prompt": "x" });
let p: ImageParams = serde_json::from_value(json).unwrap();
assert_eq!(p.width, 512);
assert_eq!(p.height, 512);
assert_eq!(p.steps, 20);
assert_eq!(p.ext, "webp");
assert_eq!(p.seed, None);
}
#[test]
fn llm_params_uses_serde_defaults_for_missing_fields() {
let json = serde_json::json!({ "messages": [] });
let p: LlmParams = serde_json::from_value(json).unwrap();
assert_eq!(p.max_tokens, 512);
assert!((p.temperature - 0.7).abs() < 1e-6);
}
#[test]
fn audio_tts_params_uses_serde_defaults_for_missing_fields() {
let json = serde_json::json!({ "text": "hi" });
let p: AudioTtsParams = serde_json::from_value(json).unwrap();
assert_eq!(p.voice, "default");
assert_eq!(p.ext, "wav");
}
#[test]
fn video_params_uses_serde_defaults_for_missing_fields() {
let json = serde_json::json!({ "prompt": "x" });
let p: VideoParams = serde_json::from_value(json).unwrap();
assert!((p.seconds - 2.0).abs() < 1e-6);
assert_eq!(p.width, 512);
assert_eq!(p.height, 512);
assert_eq!(p.ext, "mp4");
}
#[test]
fn job_claim_without_task_or_model_source_fails_to_deserialise() {
let json = serde_json::json!({
"jobId": "j-1",
"gameId": "g-1",
"assetName": "g-1/creatures/x",
"model": "synthetic",
"vramGbEstimate": 1.0,
});
let err = serde_json::from_value::<JobClaim>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("task") || msg.contains("modelSource"),
"expected missing-required-field error, got: {msg}"
);
}
#[test]
fn job_claim_with_full_task_and_model_source_round_trips() {
let json = serde_json::json!({
"jobId": "j-1",
"gameId": "g-1",
"assetName": "g-1/creatures/x",
"model": "synthetic-image",
"vramGbEstimate": 1.0,
"task": {
"kind": "image",
"prompt": "a koi",
"width": 1024,
"height": 1024,
"ext": "webp",
},
"modelSource": {
"engine": "synthetic",
"files": [],
"cliDefaults": { "cfgScale": 1.0, "steps": 8, "width": 1024, "height": 1024 },
},
});
let claim: JobClaim = serde_json::from_value(json).unwrap();
assert_eq!(claim.task.kind(), TaskKind::Image);
let res = synth_engine().dispatch(&claim.model, claim.task).unwrap();
assert_eq!(res.kind(), TaskKind::Image);
}