use amberfork_record::{CapturedRequest, CapturedResponse, Cassette};
use crate::canon::canonicalize_body;
#[derive(Debug)]
pub struct Replay {
cassette: Cassette,
}
impl Replay {
#[must_use]
pub fn new(cassette: Cassette) -> Self {
Self { cassette }
}
#[must_use]
pub fn lookup(&self, request: &CapturedRequest) -> Option<&CapturedResponse> {
let incoming_body = canonicalize_body(&request.body);
self.cassette
.exchanges
.iter()
.find(|exchange| {
let recorded = &exchange.request;
recorded.method == request.method
&& recorded.path == request.path
&& canonicalize_body(&recorded.body) == incoming_body
})
.map(|exchange| &exchange.response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use amberfork_record::{Body, Exchange};
use serde_json::{Value, json};
fn request(body: Value) -> CapturedRequest {
CapturedRequest {
method: "POST".to_string(),
path: "/v1/chat/completions".to_string(),
headers: Vec::new(),
body: Body::Json(body),
}
}
fn response(body: Value) -> CapturedResponse {
CapturedResponse {
status: 200,
headers: Vec::new(),
body: Body::Json(body),
}
}
fn one_exchange_cassette(req: CapturedRequest, resp: CapturedResponse) -> Cassette {
let mut cassette = Cassette::new("test");
cassette.exchanges.push(Exchange {
idx: 0,
request: req,
response: resp,
});
cassette
}
#[test]
fn recorded_request_returns_recorded_response() {
let recorded_request = request(json!({"messages": [{"role": "user", "content": "hi"}]}));
let recorded_response = response(json!({"choices": [{"message": {"content": "hello"}}]}));
let replay = Replay::new(one_exchange_cassette(
recorded_request.clone(),
recorded_response.clone(),
));
assert_eq!(replay.lookup(&recorded_request), Some(&recorded_response));
}
#[test]
fn body_object_key_order_does_not_defeat_a_match() {
let replay = Replay::new(one_exchange_cassette(
request(json!({"model": "claude-sonnet-5", "stream": false})),
response(json!({"ok": true})),
));
let reordered = request(json!({"stream": false, "model": "claude-sonnet-5"}));
assert!(replay.lookup(&reordered).is_some());
}
#[test]
fn a_branched_request_is_a_miss() {
let replay = Replay::new(one_exchange_cassette(
request(json!({"messages": [{"role": "user", "content": "hi"}]})),
response(json!({"choices": []})),
));
let branched = request(json!({"messages": [{"role": "user", "content": "different"}]}));
assert_eq!(replay.lookup(&branched), None);
}
#[test]
fn a_reissued_call_with_a_fresh_tool_id_still_hits() {
let recorded_response = response(json!({"choices": [{"message": {"content": "done"}}]}));
let replay = Replay::new(one_exchange_cassette(
request(json!({
"messages": [
{"role": "assistant", "tool_calls": [
{"id": "call_ABC", "type": "function",
"function": {"name": "search", "arguments": "{}"}}
]},
{"role": "tool", "tool_call_id": "call_ABC", "content": "result"}
]
})),
recorded_response.clone(),
));
let reissued = request(json!({
"messages": [
{"role": "assistant", "tool_calls": [
{"id": "call_XYZ", "type": "function",
"function": {"name": "search", "arguments": "{}"}}
]},
{"role": "tool", "tool_call_id": "call_XYZ", "content": "result"}
]
}));
assert_eq!(replay.lookup(&reissued), Some(&recorded_response));
}
}