use tokio_util::sync::CancellationToken;
use super::gateway::GatewayVideo;
use super::{VideoAnswer, VideoConfig, VideoRequest, VideoUnderstanding};
use crate::shared::config::{MediaResolution, VideoProvider};
use crate::shared::http_stub::{Step, Stub, json, refused};
const MODEL: &str = "google/gemini-3.5-flash-lite";
const VIDEO: &str = "https://www.youtube.com/watch?v=IwZVXmQdX1E";
const ENTRY: &str = r#"{"data":{"id":"google/gemini-3.5-flash-lite","context_length":1048576,
"architecture":{"input_modalities":["text","image","video","file","audio"]},
"reasoning":{"mandatory":true,"default_enabled":true,
"supported_efforts":["high","medium","low","minimal"],"default_effort":"minimal"}}}"#;
const ENTRY_WITHOUT_EFFORTS: &str = r#"{"data":{"id":"vendor/plain","context_length":32768}}"#;
const WATCHED: &str = r#"{"id":"gen-1","model":"google/gemini-3.5-flash-lite","provider":"Google",
"choices":[{"index":0,"finish_reason":"stop","native_finish_reason":"STOP",
"message":{"role":"assistant","content":"Now, bound for the moon.","reasoning":null}}],
"usage":{"prompt_tokens":6119,"completion_tokens":19,"cost":0.0018832,
"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":1672,"video_tokens":4422}}}"#;
const A_PAGE_READ: &str = r#"{"id":"gen-2","provider":"Google",
"choices":[{"index":0,"finish_reason":"stop",
"message":{"role":"assistant","content":"The video shows a NASA video detailing the plans for a moon base."}}],
"usage":{"prompt_tokens":551337,"completion_tokens":21,"cost":0.1654436,
"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0,"video_tokens":0}}}"#;
const CUT: &str = r#"{"choices":[{"finish_reason":"length",
"message":{"content":"[0:00] This is the moment"}}],
"usage":{"prompt_tokens":6119,"prompt_tokens_details":{"video_tokens":4422}}}"#;
const NOTHING_SAID: &str = r#"{"choices":[{"finish_reason":"length","message":{"content":""}}],
"usage":{"prompt_tokens":6119,"prompt_tokens_details":{"video_tokens":6094}}}"#;
const REFUSED_AS_200: &str = r#"{"id":"gen-3","error":{"message":"Invalid image URL: content_type='text/html; charset=utf-8' for url='https://www.youtube.com/watch?v=IwZVXmQdX1E'","code":400,"metadata":{"error_type":"invalid_request"}}}"#;
const NOT_THERE: &str = r#"{"error":{"message":"Provider returned error","code":502,"metadata":{"raw":"{\n \"error\": {\n \"code\": 403,\n \"message\": \"The caller does not have permission\",\n \"status\": \"PERMISSION_DENIED\"\n }\n}\n","provider_name":"Google AI Studio"}}}"#;
const NO_VIDEO_INPUT: &str = r#"{"error":{"message":"No endpoints found that support input video","code":404,"metadata":{"failed_routing_step":"Filter by Input Video Support"}}}"#;
const WRONG_KEY: &str = r#"{"error":{"message":"User not found.","code":401}}"#;
fn config(url: &str, attribution: bool) -> VideoConfig {
VideoConfig {
provider: VideoProvider::OpenRouter,
attribution,
model: format!(" {MODEL} "),
base_url: format!("{url}/"),
api_key: "sk-or-stored".into(),
media_resolution: MediaResolution::Medium,
max_minutes: 30,
}
}
fn request() -> VideoRequest {
VideoRequest {
url: VIDEO.into(),
prompt: "describe it".into(),
start_secs: Some(40),
end_secs: Some(80),
max_output_tokens: 2000,
}
}
async fn watched(client: &GatewayVideo) -> anyhow::Result<VideoAnswer> {
client.describe(request(), &CancellationToken::new()).await
}
#[tokio::test]
async fn the_link_is_a_part_of_a_chat_request_at_the_lowest_effort_listed() {
let stub = Stub::serving(vec![json(ENTRY), json(WATCHED)]).await;
let client = GatewayVideo::new(config(&stub.url, true));
let answer = watched(&client).await.expect("an answer");
assert_eq!(
answer,
VideoAnswer {
text: "Now, bound for the moon.".into(),
truncated: false,
}
);
assert!(!client.reads_segments(), "the whole video is what is read");
assert_eq!(
stub.asked(),
[
format!("get /v1/model/{MODEL} http/1.1"),
"post /v1/chat/completions http/1.1".to_string()
],
"a trailing slash in the address is not doubled, and the slug is trimmed"
);
for head in stub.requests().iter().map(|r| r.to_ascii_lowercase()) {
assert!(
head.contains("authorization: bearer sk-or-stored"),
"{head}"
);
assert!(head.contains("x-openrouter-title: mindfork"), "{head}");
assert!(head.contains("http-referer: https://mindfork.io"), "{head}");
}
assert_eq!(
stub.bodies(),
[serde_json::json!({
"model": MODEL,
"max_tokens": 2000,
"reasoning": {"effort": "minimal"},
"messages": [{"role": "user", "content": [
{"type": "text", "text": "describe it"},
{"type": "video_url", "video_url": {"url": VIDEO}},
]}],
})]
);
}
#[tokio::test]
async fn the_models_entry_is_asked_once_and_silence_about_efforts_is_silence() {
let stub = Stub::serving(vec![json(ENTRY), json(WATCHED), json(WATCHED)]).await;
let client = GatewayVideo::new(config(&stub.url, false));
watched(&client).await.expect("the first video");
watched(&client).await.expect("the second");
let asked = stub.asked();
assert_eq!(asked.len(), 3, "{asked:#?}");
assert!(asked[2].starts_with("post "), "{asked:#?}");
for head in stub.requests().iter().map(|r| r.to_ascii_lowercase()) {
assert!(
!head.contains("x-openrouter-title"),
"the switch is off: {head}"
);
assert!(!head.contains("http-referer"), "{head}");
}
for entry in [
json(ENTRY_WITHOUT_EFFORTS),
refused("404 Not Found", r#"{"error":{"message":"no such model"}}"#),
json("not an entry"),
] {
let stub = Stub::serving(vec![entry, json(WATCHED), json(WATCHED)]).await;
let client = GatewayVideo::new(config(&stub.url, true));
watched(&client).await.expect("an answer");
watched(&client).await.expect("and another");
let bodies = stub.bodies();
let sent = bodies.iter().filter(|b| b.get("messages").is_some());
assert_eq!(sent.clone().count(), 2);
assert!(
sent.clone().all(|b| b.get("reasoning").is_none()),
"{bodies:#?}"
);
assert_eq!(stub.asked().len(), 3, "what the gateway answered is kept");
}
}
#[tokio::test]
async fn an_outage_of_the_catalogue_is_asked_about_again() {
for status in ["503 Service Unavailable", "429 Too Many Requests"] {
let later = refused(status, r#"{"error":{"message":"try later"}}"#);
let script = vec![later, json(WATCHED), json(ENTRY), json(WATCHED)];
let stub = Stub::serving(script).await;
let client = GatewayVideo::new(config(&stub.url, true));
watched(&client).await.expect("watched during the outage");
watched(&client).await.expect("and after it");
let asked: Vec<String> = stub.asked().iter().map(|a| a[..4].to_string()).collect();
assert_eq!(asked, ["get ", "post", "get ", "post"], "{status}");
let efforts: Vec<Option<String>> = stub
.bodies()
.iter()
.filter(|b| b.get("messages").is_some())
.map(|b| b.pointer("/reasoning/effort")?.as_str().map(str::to_string))
.collect();
assert_eq!(efforts, [None, Some("minimal".to_string())], "{status}");
}
}
#[tokio::test]
async fn an_answer_with_no_video_tokens_is_an_error_whatever_it_says() {
const NO_USAGE: &str = r#"{"choices":[{"finish_reason":"stop","message":{"content":"A plausible description."}}]}"#;
const NO_DETAILS: &str = r#"{"choices":[{"finish_reason":"stop","message":{"content":"A plausible description."}}],
"usage":{"prompt_tokens":77}}"#;
for (body, counted) in [
(A_PAGE_READ, "551337"),
(NO_USAGE, "the 0 prompt"),
(NO_DETAILS, "77"),
] {
let stub = Stub::serving(vec![json(ENTRY), json(body)]).await;
let err = watched(&GatewayVideo::new(config(&stub.url, true)))
.await
.expect_err("no video was read");
let text = err.to_string();
assert!(text.contains("without reading the video"), "{text}");
assert!(text.contains(counted), "the count is said: {text}");
assert!(
!text.contains("NASA") && !text.contains("plausible"),
"{text}"
);
}
}
#[tokio::test]
async fn a_refusal_is_read_from_the_body_before_the_status_is_believed() {
let table: [(&'static str, &'static str, &[&str]); 6] = [
("200 OK", REFUSED_AS_200, &["200", "Invalid image URL"]),
(
"502 Bad Gateway",
NOT_THERE,
&[
"502",
"Provider returned error: The caller does not have permission",
],
),
(
"404 Not Found",
NO_VIDEO_INPUT,
&["404", "No endpoints found that support input video"],
),
("401 Unauthorized", WRONG_KEY, &["401", "User not found."]),
(
"502 Bad Gateway",
"upstream is away",
&["502", "upstream is away"],
),
("500 Internal Server Error", "{}", &["500"]),
];
for (status, body, said) in table {
let step = Step {
status,
..json(body)
};
let stub = Stub::serving(vec![json(ENTRY), step]).await;
let err = watched(&GatewayVideo::new(config(&stub.url, true)))
.await
.expect_err("a refusal");
let text = err.to_string();
for part in said {
assert!(text.contains(part), "{status}: {text}");
}
assert!(
!text.contains("metadata"),
"the sentence, not the JSON: {text}"
);
assert_eq!(stub.asked().len(), 2, "{status}: not asked again");
}
}
#[tokio::test]
async fn a_cut_answer_is_marked_and_an_empty_one_is_an_error() {
let stub = Stub::serving(vec![json(ENTRY), json(CUT), json(NOTHING_SAID)]).await;
let client = GatewayVideo::new(config(&stub.url, true));
let cut = watched(&client).await.expect("a prefix is an answer");
assert!(cut.truncated);
assert_eq!(cut.text, "[0:00] This is the moment");
let err = watched(&client).await.expect_err("nothing was said");
assert!(err.to_string().contains("finish reason: length"), "{err}");
}
#[tokio::test]
async fn a_cancelled_request_is_not_sent() {
let stub = Stub::serving(vec![json(ENTRY), json(WATCHED)]).await;
let cancel = CancellationToken::new();
cancel.cancel();
let err = GatewayVideo::new(config(&stub.url, true))
.describe(request(), &cancel)
.await
.expect_err("cancelled");
assert!(err.to_string().contains("cancelled"), "{err}");
assert!(stub.asked().is_empty(), "{:?}", stub.asked());
}