#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::collections::BTreeMap;
use std::path::PathBuf;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use http::HeaderMap;
use serde::Deserialize;
use super::{HARNESS_ID_UNKNOWN, TapesAttribution};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Roundtrip,
Encode,
Decode,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct FixtureCase {
pub name: String,
pub direction: String,
pub headers: BTreeMap<String, String>,
pub envelope: FixtureEnvelope,
#[serde(default)]
pub encode_from: Option<FixtureEnvelope>,
}
impl FixtureCase {
#[must_use]
pub fn direction(&self) -> Direction {
match self.direction.as_str() {
"roundtrip" => Direction::Roundtrip,
"encode" => Direction::Encode,
"decode" => Direction::Decode,
other => panic!("{}: unknown direction {other:?}", self.name),
}
}
#[must_use]
pub fn logical_envelope(&self) -> &FixtureEnvelope {
assert!(
self.encode_from.is_none() || self.direction() == Direction::Encode,
"{}: encode_from is reserved for lossy `encode` cases, but direction is {:?}",
self.name,
self.direction,
);
self.encode_from.as_ref().unwrap_or(&self.envelope)
}
#[must_use]
pub fn expected_tapes_headers(&self) -> BTreeMap<String, String> {
self.headers
.iter()
.filter(|(name, _)| name.starts_with("x-tapes-"))
.map(|(name, value)| (name.clone(), value.clone()))
.collect()
}
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct FixtureEnvelope {
#[serde(default)]
pub harness_id: Option<String>,
#[serde(default)]
pub harness_session_id: Option<String>,
#[serde(default)]
pub harness_version: Option<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub parent_harness_session_id: Option<String>,
#[serde(default)]
pub harness_metadata: Option<serde_json::Value>,
}
#[must_use]
pub fn cases_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("vendor")
.join("tapes-envelope-fixtures")
.join("cases")
}
#[must_use]
pub fn load_cases() -> Vec<FixtureCase> {
let dir = cases_dir();
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read {}: {e}", dir.display()))
.map(|entry| entry.expect("read dir entry").path())
.filter(|p| p.extension().is_some_and(|e| e == "json"))
.collect();
paths.sort();
assert!(
!paths.is_empty(),
"no envelope fixture cases under {} — run scripts/sync-envelope-fixtures.sh <tapes-checkout>",
dir.display(),
);
paths
.iter()
.map(|p| {
let bytes = std::fs::read(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {}: {e}", p.display()))
})
.collect()
}
#[must_use]
pub fn attribution_from(env: &FixtureEnvelope) -> TapesAttribution {
let metadata = match &env.harness_metadata {
Some(serde_json::Value::Object(map)) => map.clone(),
_ => serde_json::Map::new(),
};
TapesAttribution {
harness_id: env
.harness_id
.clone()
.unwrap_or_else(|| HARNESS_ID_UNKNOWN.to_owned()),
session_id: env.harness_session_id.clone(),
version: env.harness_version.clone(),
cwd: env.cwd.clone(),
name: env.name.clone(),
parent_sid: env.parent_harness_session_id.clone(),
metadata,
}
}
#[must_use]
pub fn tapes_headers(headers: &HeaderMap) -> BTreeMap<String, String> {
headers
.iter()
.filter(|(name, _)| name.as_str().starts_with("x-tapes-"))
.map(|(name, value)| {
let v = value
.to_str()
.expect("emitted header value must be visible ASCII")
.to_owned();
(name.as_str().to_owned(), v)
})
.collect()
}
#[must_use]
pub fn decode_metadata(encoded: &str) -> serde_json::Value {
let raw = URL_SAFE_NO_PAD
.decode(encoded)
.unwrap_or_else(|e| panic!("metadata header is not base64url(no-pad): {e}"));
serde_json::from_slice(&raw)
.unwrap_or_else(|e| panic!("metadata header does not decode to JSON: {e}"))
}
#[cfg(test)]
use crate::envelope::{
X_TAPES_HARNESS_METADATA, inject_tapes_attribution, inject_unattributed_envelope,
};
#[cfg(test)]
#[test]
fn produces_every_encodable_fixture_case() {
let cases = load_cases();
assert!(
cases.len() >= 15,
"only {} envelope fixture cases loaded; the vendored corpus looks truncated",
cases.len(),
);
let mut produced = 0_usize;
let mut skipped = Vec::new();
for case in &cases {
if case.direction() == Direction::Decode {
skipped.push(case.name.clone());
continue;
}
let logical = case.logical_envelope();
let mut headers = HeaderMap::new();
inject_tapes_attribution(&mut headers, attribution_from(logical))
.unwrap_or_else(|e| panic!("{}: inject failed: {e:?}", case.name));
let got = tapes_headers(&headers);
let want = case.expected_tapes_headers();
let got_names: Vec<&String> = got.keys().collect();
let want_names: Vec<&String> = want.keys().collect();
assert_eq!(
got_names, want_names,
"{}: emitted header set does not match the fixture",
case.name,
);
for (name, want_value) in &want {
let got_value = &got[name];
if name == X_TAPES_HARNESS_METADATA {
assert_eq!(
decode_metadata(got_value),
decode_metadata(want_value),
"{}: {name} decodes to different JSON",
case.name,
);
} else {
assert_eq!(got_value, want_value, "{}: {name}", case.name);
}
}
for name in headers.keys() {
assert!(
!name.as_str().starts_with("x-paper-auth-"),
"{}: producer emitted server-trusted header {name}",
case.name,
);
}
produced += 1;
}
preserves_complete_inbound_envelopes(&cases);
assert_eq!(
produced + skipped.len(),
cases.len(),
"every case must be either produced or explicitly skipped",
);
assert!(
produced >= 10,
"only {produced} cases exercised the producer; skipped: {skipped:?}",
);
}
#[cfg(test)]
#[test]
fn unknown_harness_case_emits_only_the_required_header() {
let case = load_cases()
.into_iter()
.find(|c| c.name == "unknown-bare")
.expect("corpus contains the unknown-bare case");
let mut headers = HeaderMap::new();
inject_tapes_attribution(&mut headers, attribution_from(&case.envelope)).unwrap();
let got = tapes_headers(&headers);
assert_eq!(got.len(), 1, "unknown harness attaches exactly one header");
assert_eq!(got["x-tapes-harness-id"], HARNESS_ID_UNKNOWN);
}
#[cfg(test)]
fn preserves_complete_inbound_envelopes(cases: &[FixtureCase]) {
let mut checked = 0;
for case in cases {
let inbound: BTreeMap<String, String> = case
.headers
.iter()
.map(|(k, v)| (k.to_ascii_lowercase(), v.clone()))
.collect();
let harness_id = inbound
.get("x-tapes-harness-id")
.map(|v| v.trim())
.filter(|v| !v.is_empty() && *v != HARNESS_ID_UNKNOWN);
let session_id = inbound
.get("x-tapes-harness-session-id")
.map(|v| v.trim())
.filter(|v| !v.is_empty());
if harness_id.is_none() || session_id.is_none() {
continue;
}
let mut headers = HeaderMap::new();
for (name, value) in &case.headers {
let parsed_name = match http::HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
Err(_) => continue,
};
let Ok(parsed_value) = http::HeaderValue::from_str(value) else {
continue;
};
headers.insert(parsed_name, parsed_value);
}
let before = tapes_headers(&headers);
if before.get("x-tapes-harness-id").map(String::as_str) != harness_id {
continue;
}
inject_unattributed_envelope(&mut headers).unwrap_or_else(|e| {
panic!("{}: inject_unattributed_envelope failed: {e:?}", case.name)
});
assert_eq!(
tapes_headers(&headers),
before,
"{}: a complete inbound envelope must be preserved as-is",
case.name,
);
checked += 1;
}
assert!(
checked > 0,
"no case exercised the complete-inbound-envelope preservation path; \
the corpus should retain at least one (e.g. pi-complete)",
);
}