use amberfork_model::DiffResult;
use amberfork_record::Cassette;
#[must_use]
pub fn patch_cassette(diff: &DiffResult, bad: &Cassette, good: &Cassette) -> Option<Cassette> {
let fork = diff.fork?;
patch_many(bad, good, &[(fork.b_step?, fork.a_step?)])
}
pub(crate) fn patch_many(
bad: &Cassette,
good: &Cassette,
grafts: &[(usize, usize)],
) -> Option<Cassette> {
let mut patched = bad.clone();
for &(bad_step, good_step) in grafts {
let good_response = good.exchanges.get(good_step)?.response.clone();
patched.exchanges.get_mut(bad_step)?.response = good_response;
}
Some(patched)
}
#[cfg(test)]
mod tests {
use super::*;
use amberfork_model::{Fork, Meta, RunPair, RunRef, Source};
use amberfork_record::{Body, CapturedRequest, CapturedResponse, Exchange};
use serde_json::json;
fn diff_with_fork(fork: Option<Fork>) -> DiffResult {
DiffResult {
runs: RunPair {
a: run_ref("good"),
b: run_ref("bad"),
},
alignment: Vec::new(),
fork,
field_diffs: Vec::new(),
attribution: None,
deltas: None,
warnings: Vec::new(),
meta: Meta::current(Source::Record),
}
}
fn run_ref(id: &str) -> RunRef {
RunRef {
id: id.to_string(),
task: None,
outcome: None,
n_steps: 0,
}
}
fn exchange(idx: usize, question: &str, answer: &str) -> Exchange {
Exchange {
idx,
request: CapturedRequest {
method: "POST".to_string(),
path: "/v1/messages".to_string(),
headers: Vec::new(),
body: Body::Json(json!({ "content": question })),
},
response: CapturedResponse {
status: 200,
headers: Vec::new(),
body: Body::Json(json!({ "content": answer })),
},
}
}
fn cassette(id: &str, exchanges: Vec<Exchange>) -> Cassette {
let mut cassette = Cassette::new(id);
cassette.exchanges = exchanges;
cassette
}
fn fork_at(index: usize, a_step: Option<usize>, b_step: Option<usize>) -> Fork {
Fork {
index,
a_step,
b_step,
confidence: 0.9,
}
}
#[test]
fn the_good_response_lands_at_the_fork_step_and_no_other_exchange_moves() {
let bad = cassette(
"bad",
vec![
exchange(0, "q0", "bad-a0"),
exchange(1, "q1", "bad-a1"), exchange(2, "q2", "bad-a2"),
],
);
let good = cassette(
"good",
vec![
exchange(0, "q0", "good-a0"),
exchange(1, "q1", "good-a1"), exchange(2, "q2", "good-a2"),
],
);
let patched = patch_cassette(
&diff_with_fork(Some(fork_at(1, Some(1), Some(1)))),
&bad,
&good,
)
.expect("a two-sided fork produces a patch");
assert_eq!(patched.exchanges[1].response, good.exchanges[1].response);
assert_eq!(patched.exchanges[1].request, bad.exchanges[1].request);
assert_eq!(patched.exchanges[0], bad.exchanges[0]);
assert_eq!(patched.exchanges[2], bad.exchanges[2]);
assert_eq!(patched.id, bad.id);
assert_eq!(patched.exchanges.len(), bad.exchanges.len());
}
#[test]
fn a_one_sided_fork_has_no_pair_to_patch() {
let bad = cassette("bad", vec![exchange(0, "q0", "bad")]);
let good = cassette("good", vec![exchange(0, "q0", "good")]);
let log_only = fork_at(0, None, Some(0));
assert!(patch_cassette(&diff_with_fork(Some(log_only)), &bad, &good).is_none());
let model_only = fork_at(0, Some(0), None);
assert!(patch_cassette(&diff_with_fork(Some(model_only)), &bad, &good).is_none());
}
#[test]
fn a_converged_diff_has_nothing_to_patch() {
let bad = cassette("bad", vec![exchange(0, "q0", "same")]);
let good = cassette("good", vec![exchange(0, "q0", "same")]);
assert!(patch_cassette(&diff_with_fork(None), &bad, &good).is_none());
}
#[test]
fn patch_many_grafts_every_listed_step_and_leaves_the_rest_alone() {
let bad = cassette(
"bad",
vec![
exchange(0, "q0", "bad-a0"),
exchange(1, "q1", "bad-a1"),
exchange(2, "q2", "bad-a2"),
exchange(3, "q3", "bad-a3"),
],
);
let good = cassette(
"good",
vec![
exchange(0, "q0", "good-a0"),
exchange(1, "q1", "good-a1"),
exchange(2, "q2", "good-a2"),
exchange(3, "q3", "good-a3"),
],
);
let patched = patch_many(&bad, &good, &[(1, 1), (3, 3)]).expect("in-bounds grafts patch");
assert_eq!(patched.exchanges[0], bad.exchanges[0]);
assert_eq!(patched.exchanges[1].response, good.exchanges[1].response);
assert_eq!(patched.exchanges[1].request, bad.exchanges[1].request);
assert_eq!(patched.exchanges[2], bad.exchanges[2]);
assert_eq!(patched.exchanges[3].response, good.exchanges[3].response);
assert_eq!(patched.id, bad.id);
}
#[test]
fn patch_many_with_an_out_of_bounds_graft_yields_no_patch() {
let bad = cassette("bad", vec![exchange(0, "q0", "bad")]);
let good = cassette("good", vec![exchange(0, "q0", "good")]);
assert!(patch_many(&bad, &good, &[(0, 0), (5, 0)]).is_none());
}
#[test]
fn a_fork_index_past_the_cassette_yields_no_patch_rather_than_panicking() {
let bad = cassette("bad", vec![exchange(0, "q0", "bad")]);
let good = cassette("good", vec![exchange(0, "q0", "good")]);
let out_of_range = fork_at(9, Some(0), Some(3));
assert!(patch_cassette(&diff_with_fork(Some(out_of_range)), &bad, &good).is_none());
}
}