use std::sync::Mutex;
use super::client::{VerifyClient, VerifyError};
use super::types::{GetVerifySessionResponse, PostVerifySessionResponse, VerifySessionRequest};
pub struct MockVerifyClient {
post_response: Mutex<Option<Result<PostVerifySessionResponse, VerifyError>>>,
get_responses: Mutex<Vec<Result<GetVerifySessionResponse, VerifyError>>>,
posted: Mutex<Vec<VerifySessionRequest>>,
fetched: Mutex<Vec<(String, Option<u32>)>>,
}
impl MockVerifyClient {
pub fn with_post_response(resp: PostVerifySessionResponse) -> Self {
Self {
post_response: Mutex::new(Some(Ok(resp))),
get_responses: Mutex::new(Vec::new()),
posted: Mutex::new(Vec::new()),
fetched: Mutex::new(Vec::new()),
}
}
pub fn with_post_error(err: VerifyError) -> Self {
Self {
post_response: Mutex::new(Some(Err(err))),
get_responses: Mutex::new(Vec::new()),
posted: Mutex::new(Vec::new()),
fetched: Mutex::new(Vec::new()),
}
}
pub fn with_get_responses(get_responses: Vec<GetVerifySessionResponse>) -> Self {
Self {
post_response: Mutex::new(Some(Ok(PostVerifySessionResponse {
session_id: "mock-session".into(),
view_url: "https://mock.aretta.ai/dashboard/jobs/mock-session".into(),
plan_size: 0,
}))),
get_responses: Mutex::new(get_responses.into_iter().map(Ok).collect::<Vec<_>>()),
posted: Mutex::new(Vec::new()),
fetched: Mutex::new(Vec::new()),
}
}
pub fn with_post_and_gets(
post: PostVerifySessionResponse,
get_responses: Vec<GetVerifySessionResponse>,
) -> Self {
Self {
post_response: Mutex::new(Some(Ok(post))),
get_responses: Mutex::new(get_responses.into_iter().map(Ok).collect::<Vec<_>>()),
posted: Mutex::new(Vec::new()),
fetched: Mutex::new(Vec::new()),
}
}
pub fn posted_requests(&self) -> Vec<VerifySessionRequest> {
self.posted.lock().expect("mock mutex").clone()
}
pub fn fetched_sessions(&self) -> Vec<(String, Option<u32>)> {
self.fetched.lock().expect("mock mutex").clone()
}
}
impl std::fmt::Debug for MockVerifyClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MockVerifyClient")
.field(
"posted_count",
&self.posted.lock().map(|v| v.len()).unwrap_or(0),
)
.field(
"fetched_count",
&self.fetched.lock().map(|v| v.len()).unwrap_or(0),
)
.finish()
}
}
impl VerifyClient for MockVerifyClient {
fn post_session(
&self,
req: &VerifySessionRequest,
) -> Result<PostVerifySessionResponse, VerifyError> {
self.posted.lock().expect("mock mutex").push(req.clone());
let mut slot = self.post_response.lock().expect("mock mutex");
match slot.take() {
Some(Ok(r)) => Ok(r),
Some(Err(e)) => Err(e),
None => panic!("MockVerifyClient: post_session called more than once"),
}
}
fn get_session(
&self,
session_id: &str,
wait_seconds: Option<u32>,
) -> Result<GetVerifySessionResponse, VerifyError> {
self.fetched
.lock()
.expect("mock mutex")
.push((session_id.to_string(), wait_seconds));
let mut queue = self.get_responses.lock().expect("mock mutex");
if queue.is_empty() {
panic!("MockVerifyClient: get_session called but no canned response remains");
}
queue.remove(0)
}
}
#[cfg(test)]
mod tests {
use super::super::types::{
AnnotationOutcomeStatus, GetVerifySessionResponse, SessionStatus, VerifySessionSummary,
VerifySessionTag,
};
use super::*;
fn one_tag() -> VerifySessionTag {
VerifySessionTag {
annotation_id: "arta_x".into(),
canon_id: "foo".into(),
version: "v0.1.0".into(),
source_path: "src/x.rs:1".into(),
}
}
fn done_response(summary: VerifySessionSummary) -> GetVerifySessionResponse {
GetVerifySessionResponse {
session_id: "mock-session".into(),
status: SessionStatus::Done,
user_commit_sha: "abc".into(),
books_commit_sha: None,
canon_version: "v0.1.0".into(),
started_at: "2026-05-24T00:00:00Z".into(),
completed_at: Some("2026-05-24T00:01:00Z".into()),
annotations: vec![],
summary,
}
}
#[test]
fn post_records_request_and_returns_canned_response() {
let mock = MockVerifyClient::with_post_response(PostVerifySessionResponse {
session_id: "sid".into(),
view_url: "https://x/y".into(),
plan_size: 1,
});
let req = VerifySessionRequest {
repo_full_name: "owner/repo".into(),
commit_sha: "deadbeef".into(),
tags: vec![one_tag()],
};
let r = mock.post_session(&req).unwrap();
assert_eq!(r.session_id, "sid");
let posted = mock.posted_requests();
assert_eq!(posted.len(), 1);
assert_eq!(posted[0].repo_full_name, "owner/repo");
assert_eq!(posted[0].tags.len(), 1);
}
#[test]
#[should_panic(expected = "post_session called more than once")]
fn post_twice_panics_to_surface_test_misconfig() {
let mock = MockVerifyClient::with_post_response(PostVerifySessionResponse {
session_id: "sid".into(),
view_url: "https://x/y".into(),
plan_size: 0,
});
let req = VerifySessionRequest {
repo_full_name: "o/r".into(),
commit_sha: "x".into(),
tags: vec![],
};
mock.post_session(&req).unwrap();
let _ = mock.post_session(&req);
}
#[test]
fn get_replays_canned_responses_in_order() {
let r0 = done_response(VerifySessionSummary {
total_annotations: 0,
verified: 0,
failed: 0,
build_failed: 0,
inconclusive: 0,
no_coverage: 0,
});
let r1 = done_response(VerifySessionSummary {
total_annotations: 1,
verified: 1,
failed: 0,
build_failed: 0,
inconclusive: 0,
no_coverage: 0,
});
let mock = MockVerifyClient::with_get_responses(vec![r0.clone(), r1.clone()]);
assert_eq!(mock.get_session("sid", None).unwrap(), r0);
assert_eq!(mock.get_session("sid", Some(30)).unwrap(), r1);
let fetched = mock.fetched_sessions();
assert_eq!(
fetched,
vec![("sid".into(), None), ("sid".into(), Some(30))]
);
}
#[test]
fn post_error_propagates() {
let mock = MockVerifyClient::with_post_error(VerifyError::BadRequest {
status: 402,
message: "no_canon_coverage".into(),
});
let req = VerifySessionRequest {
repo_full_name: "o/r".into(),
commit_sha: "x".into(),
tags: vec![one_tag()],
};
match mock.post_session(&req).unwrap_err() {
VerifyError::BadRequest { status: 402, .. } => {}
other => panic!("expected BadRequest 402, got {other:?}"),
}
assert_eq!(mock.posted_requests().len(), 1);
}
#[test]
fn placeholder_status_consumed() {
let _ = AnnotationOutcomeStatus::Verified;
}
}