A hop sent to a third party's endpoint is work the router did not do. Its OER/2 receipt says the hop served or failed; nothing WAI defines says what was sent, to whom, what came back, or what the provider bills, so an implementation carries that in a record of its own that WAI's verifier cannot read. `wai-route-third-party-hop`, signed by the router and named by its digest: the provider and its API origin, whose account the hop ran on (never the key), the provider's id for the work, each exchange (method, URL without its query, times, status, the bodies by SHA-256 and length, the response where it was JSON, with links' queries removed), the outputs by name and content hash, the usage the provider bills, in the endpoint's unit, and what it reported charging, in its own terms. A verification document may carry the records beside its receipts (§7.2 step 9). Each is held to the route record's signer and job, to its attempt and that attempt's candidate, and when it served, its outputs to what the hop's receipt names and the attempt's count. No route record changes, since the record names the receipt the route record already names. routes corpus: 126 -> 202 vectors, from fixed seeds, every control refused at its own place. route_record_verify.py verifies each from the text, shares no code with wai-rs, and catches each of its 85 checks dropped and all 1,051 mutated copies. Co-Authored-By: Claude Opus 5.5 <[email protected]>
210 lines
12 KiB
Rust
210 lines
12 KiB
Rust
//! `wai.route.record` (extensions/route-record.md): the committed corpus
|
|
//! `live-conformance/routes/vectors.json` regenerates byte for byte from its seeds and is the
|
|
//! only file there besides its README; every vector gets the outcome it states; every reason
|
|
//! the extension adds and every code it reuses refuses a vector; the accepted vectors are the
|
|
//! cases §10 names, of version 1 and of version 2; a record a router seals verifies, and an
|
|
//! edit of any of its documents or of the record is refused; and §8 lists exactly the reasons
|
|
//! the reference reports.
|
|
#![cfg(feature = "route_record")]
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::PathBuf;
|
|
|
|
use serde_json::Value;
|
|
use wai::route_record::{self, corpus, Reason};
|
|
|
|
fn wai_dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
}
|
|
|
|
fn committed() -> String {
|
|
std::fs::read_to_string(wai_dir().join("live-conformance/routes/vectors.json")).expect("the corpus")
|
|
}
|
|
|
|
fn doc() -> Value {
|
|
serde_json::from_str(&committed()).unwrap()
|
|
}
|
|
|
|
fn vector(d: &Value, name: &str) -> Value {
|
|
d["vectors"].as_array().unwrap().iter().find(|v| v["name"] == name).unwrap_or_else(|| panic!("no vector {name}")).clone()
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_corpus_regenerates_from_its_seeds() {
|
|
assert!(committed() == corpus::generate().expect("the corpus generates"), "live-conformance/routes/vectors.json differs from `wai_route_record_vectors gen`");
|
|
let mut files: Vec<String> = std::fs::read_dir(wai_dir().join("live-conformance/routes")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
|
files.sort();
|
|
assert_eq!(files, ["README.md", "vectors.json"]);
|
|
}
|
|
|
|
#[test]
|
|
fn every_vector_gets_its_stated_outcome() {
|
|
let fails = corpus::verify_corpus(&committed());
|
|
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn the_corpus_refuses_every_reason() {
|
|
let d = doc();
|
|
let seen: BTreeSet<&str> = d["vectors"].as_array().unwrap().iter().filter_map(|v| v["outcome"]["reason"].as_str()).collect();
|
|
for r in Reason::ALL.iter().chain(Reason::REUSED.iter()) {
|
|
assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}");
|
|
}
|
|
}
|
|
|
|
/// §10's third-party vectors: what each record states is read, in the order of its hops, and a
|
|
/// record is only as good as its signature and its hop.
|
|
#[test]
|
|
fn the_third_party_vectors_are_the_cases_section_10_names() {
|
|
let d = doc();
|
|
let outcome = |name: &str| vector(&d, name)["outcome"].clone();
|
|
let one = outcome("third_party_served_on_the_operators_account");
|
|
assert_eq!(one["outcome"], "accept");
|
|
let read = &one["third_party"][0];
|
|
assert_eq!((read["hop"].as_u64(), read["account"].as_str(), read["usage"]["unit"].as_str(), &read["charge"]), (Some(1), Some("operator"), Some("subpixels"), &Value::Null));
|
|
assert_eq!(outcome("third_party_served_with_the_providers_charge_and_its_job")["third_party"][0]["charge"]["amount"], "0.0345");
|
|
let billed = outcome("third_party_a_failure_the_provider_bills_beside_a_served_hop");
|
|
assert_eq!(billed["third_party"][0]["usage"]["quantity"], 1_000, "what the provider bills for a failure");
|
|
assert_eq!(billed["third_party"].as_array().unwrap().len(), 2);
|
|
assert!(outcome("third_party_a_failure_it_does_not_bill")["third_party"][0]["usage"].is_null());
|
|
let hops: Vec<u64> = outcome("third_party_records_given_out_of_order")["third_party"].as_array().unwrap().iter().map(|t| t["hop"].as_u64().unwrap()).collect();
|
|
assert_eq!(hops, [1, 2], "the order of their hops, not of the document");
|
|
assert_eq!(outcome("third_party_on_the_customers_account")["third_party"][0]["account"], "customer");
|
|
assert_eq!(outcome("third_party_under_a_record_of_version_2")["outcome"], "accept");
|
|
assert_eq!(outcome("third_party_no_record_given")["third_party"], serde_json::json!([]));
|
|
// A document with no such member reads as before, its outcome with none.
|
|
assert!(outcome("v3_served_one_output").get("third_party").is_none());
|
|
// A kept response holds a signed link's URL without its query, and a fraction as its text.
|
|
let kept = vector(&d, "third_party_a_response_as_a_record_keeps_it");
|
|
let text = kept["document"]["third_party"][0].as_str().unwrap();
|
|
assert!(text.contains("https://cdn.provider.example/v.mp4\"") && !text.contains("bearer") && text.contains("\"0.4\""));
|
|
// The three reasons, each from a control of one defect.
|
|
for (name, reason) in [
|
|
("third_party_a_key_stated", "third_party_invalid"),
|
|
("third_party_a_url_with_its_query", "third_party_invalid"),
|
|
("third_party_a_response_keeping_a_signed_link", "third_party_invalid"),
|
|
("third_party_another_routers", "third_party_mismatch"),
|
|
("third_party_a_usage_in_another_unit", "third_party_mismatch"),
|
|
("third_party_outputs_the_receipt_does_not_name", "third_party_output_mismatch"),
|
|
("third_party_another_number_than_the_attempts_count", "third_party_output_mismatch"),
|
|
] {
|
|
assert_eq!(outcome(name)["reason"], reason, "{name}");
|
|
}
|
|
}
|
|
|
|
/// The accepted vectors are §10's: a billed first try, a failover whose failed hop is unmetered
|
|
/// and not billed, a hop past the deadline not billed, every allowed survivor failed, no
|
|
/// survivor at all, each way a decision is decided, and every limit removing a candidate.
|
|
#[test]
|
|
fn the_accepted_vectors_are_the_cases_section_10_names() {
|
|
let d = doc();
|
|
let o = |n: &str| vector(&d, n)["outcome"].clone();
|
|
let first = o("served_first_try");
|
|
assert_eq!((first["served"]["endpoint"].as_str(), first["billed"].as_u64(), first["deadline"].as_str()), (Some("im.alpha"), Some(1), Some("met")));
|
|
assert_eq!(first["unmetered"], serde_json::json!([]));
|
|
let fo = o("failover_after_a_server_error");
|
|
assert_eq!((fo["served"]["hop"].as_u64(), fo["billed"].as_u64()), (Some(2), Some(2)));
|
|
assert_eq!(fo["unmetered"], serde_json::json!([1]), "the failed hop's receipt states the unmetered marker");
|
|
let late = o("served_after_the_deadline");
|
|
assert_eq!((late["billed"].clone(), late["deadline"].as_str()), (Value::Null, Some("missed")));
|
|
let failed = o("every_allowed_survivor_failed");
|
|
assert_eq!((failed["served"].clone(), failed["billed"].clone()), (Value::Null, Value::Null));
|
|
assert_eq!(failed["order"].as_array().unwrap().len(), 3, "three survivors, two allowed");
|
|
assert_eq!(o("no_candidate_survives")["decision"]["decided_by"], "none");
|
|
for (n, by) in [
|
|
("served_first_try", "price"),
|
|
("decided_by_the_targets", "targets"),
|
|
("decided_by_the_sort", "ttfb"),
|
|
("an_unknown_energy_ranks_last", "energy"),
|
|
("decided_by_the_endpoint", "endpoint"),
|
|
("decided_by_quality", "quality"),
|
|
("decided_by_the_policys_second_key", "quality"),
|
|
("only_survivor", "only_survivor"),
|
|
] {
|
|
assert_eq!(o(n)["decision"]["decided_by"], by, "{n}");
|
|
}
|
|
assert_eq!(o("an_unknown_energy_ranks_last")["order"].as_array().unwrap().last().unwrap(), "av1.beta", "the survivor with no energy figure ranks last");
|
|
assert_eq!(o("receipts_not_checked")["receipts"], "not_checked");
|
|
let mut limits = BTreeSet::new();
|
|
for v in d["vectors"].as_array().unwrap().iter().filter(|v| v["outcome"]["outcome"] == "accept") {
|
|
for r in v["outcome"]["removed"].as_array().unwrap() {
|
|
for l in r["limits"].as_array().unwrap() {
|
|
limits.insert(l.as_str().unwrap().to_string());
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(limits, route_record::LIMITS.iter().map(|l| l.to_string()).collect::<BTreeSet<_>>(), "every limit removes a candidate");
|
|
}
|
|
|
|
/// Version 2's accepted vectors are §10's: each endpoint ranked by what one served job costs at
|
|
/// it, failures included (an endpoint that fails ranked after one of the same list price, a
|
|
/// cheaper one that fails more after both, the unmeasured, the thin basis and the failed one
|
|
/// after every measured endpoint); a decode at its own price; the time to first byte and the
|
|
/// targets read per successful job; the ceiling the list cost; a book with no failure measured
|
|
/// ranked by the list cost; and the served hop's outputs, one or three, stated in the outcome.
|
|
#[test]
|
|
fn the_version_2_vectors_are_the_cases_section_10_names() {
|
|
let d = doc();
|
|
let o = |n: &str| vector(&d, n)["outcome"].clone();
|
|
let order = |n: &str| -> Vec<String> { o(n)["order"].as_array().unwrap().iter().map(|x| x.as_str().unwrap().to_string()).collect() };
|
|
let cost = o("v2_ranked_by_the_expected_cost");
|
|
assert_eq!(order("v2_ranked_by_the_expected_cost"), ["im.alpha", "im.eta", "im.iota", "im.beta", "im.zeta", "im.theta", "im.gamma", "im.epsilon", "im.delta", "av1.alpha"]);
|
|
assert_eq!(cost["decision"], serde_json::json!({"decided_by": "price", "runner_up": "im.eta"}));
|
|
assert_eq!(cost["served"]["outputs"].as_array().unwrap().len(), 1);
|
|
assert_eq!(order("v2_a_decode_at_its_own_price"), ["im.alpha", "im.beta"], "the decodes alone, each at its own price, under a cap the encodes exceed");
|
|
assert_eq!(order("v2_ranked_by_the_expected_time_to_first_byte")[..6], ["im.theta", "im.iota", "im.alpha", "im.zeta", "im.beta", "im.eta"]);
|
|
assert_eq!(o("v2_the_targets_read_the_expected_time_to_first_byte")["decision"], serde_json::json!({"decided_by": "targets", "runner_up": "im.alpha"}));
|
|
assert!(order("v2_the_ceiling_reads_the_list_cost").contains(&"im.zeta".to_string()), "a list cost under the ceiling survives whatever its failures add");
|
|
assert!(!order("v2_the_ceiling_reads_the_list_cost").contains(&"im.theta".to_string()));
|
|
assert_eq!(order("v2_no_failure_measured_ranks_by_the_list_cost")[..4], ["im.gamma", "im.epsilon", "im.zeta", "im.delta"]);
|
|
assert_eq!(o("v2_served_three_outputs")["served"]["outputs"].as_array().unwrap().len(), 3);
|
|
let fo = o("v2_failover_whose_failed_hop_delivered_nothing");
|
|
assert_eq!((fo["served"]["endpoint"].as_str(), fo["billed"].as_u64()), (Some("im.eta"), Some(2)));
|
|
let late = o("v2_served_after_the_deadline");
|
|
assert_eq!((late["billed"].clone(), late["deadline"].as_str(), late["served"]["outputs"].as_array().map(Vec::len)), (Value::Null, Some("missed"), Some(1)));
|
|
assert_eq!(o("v2_receipts_not_checked")["receipts"], "not_checked");
|
|
// Version 1's outcomes state no outputs.
|
|
assert!(o("served_first_try")["served"].get("outputs").is_none());
|
|
}
|
|
|
|
/// A record verifies only against the documents it was routed from: each of them, and the
|
|
/// record, changed by one byte is refused.
|
|
#[test]
|
|
fn a_record_is_bound_to_its_documents() {
|
|
let d = doc();
|
|
for name in ["served_first_try", "v2_ranked_by_the_expected_cost"] {
|
|
let good = vector(&d, name)["document"].clone();
|
|
assert!(route_record::verify(&good).is_ok());
|
|
for member in ["request", "pricebook", "policy", "record"] {
|
|
let t = good[member].as_str().unwrap();
|
|
// A digit of a figure, changed: still a document, no longer the one routed.
|
|
let at = t.find(|c: char| c.is_ascii_digit() && c != '0').unwrap();
|
|
let mut b = t.as_bytes().to_vec();
|
|
b[at] = if b[at] == b'9' { b'8' } else { b[at] + 1 };
|
|
let mut x = good.clone();
|
|
x[member] = Value::from(String::from_utf8(b).unwrap());
|
|
assert!(route_record::verify(&x).is_err(), "{name}: {member} changed");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `wai route verify` (the catalogue's `route.verify`) prints a document's outcome, the
|
|
/// corpus's, and exits 0 when the record is accepted and 1 when it is refused.
|
|
#[test]
|
|
fn route_verify_prints_the_outcome_and_exits_by_it() {
|
|
let d = doc();
|
|
let dir = std::env::temp_dir().join(format!("wai-route-verify-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
for (name, code) in [("failover_after_a_server_error", 0), ("a_failed_hop_billed", 1), ("a_hop_receipt_not_given", 1), ("v2_served_three_outputs", 0), ("v2_a_receipt_moved_from_another_job", 1)] {
|
|
let v = vector(&d, name);
|
|
let p = dir.join(format!("{name}.json"));
|
|
std::fs::write(&p, serde_json::to_vec(&v["document"]).unwrap()).unwrap();
|
|
let o = std::process::Command::new(env!("CARGO_BIN_EXE_wai")).args(["route", "verify"]).arg(&p).output().unwrap();
|
|
assert_eq!(o.status.code(), Some(code), "{name}: {}", String::from_utf8_lossy(&o.stderr));
|
|
let got: Value = serde_json::from_slice(&o.stdout).unwrap();
|
|
assert_eq!(got, v["outcome"], "{name}");
|
|
}
|
|
let o = std::process::Command::new(env!("CARGO_BIN_EXE_wai")).args(["route", "verify"]).arg(dir.join("none.json")).output().unwrap();
|
|
assert_ne!(o.status.code(), Some(0), "a document that is not there is no accepted record");
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|