#![allow(
clippy::unwrap_used,
reason = "test helpers; a panic is a test failure"
)]
use std::fs;
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use yapi_types::auth::AuthFile;
use yapi_types::config::ConfigFile;
use yapi_types::json;
use yapi_types::message::Message;
use yapi_types::models::ModelsConfig;
use yapi_types::session::FileEntry;
use yapi_types::settings::Settings;
fn fixtures(dir: &str) -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/pi")
.join(dir);
let mut paths: Vec<PathBuf> = fs::read_dir(&dir)
.unwrap_or_else(|err| panic!("{}: {err}", dir.display()))
.map(|entry| entry.unwrap().path())
.collect();
paths.sort();
assert!(!paths.is_empty(), "no fixtures in {}", dir.display());
paths
}
fn config_kind(path: &Path) -> ConfigFile {
match path.file_name().unwrap().to_str().unwrap() {
"settings.json" | "settings-all-fields.json" => ConfigFile::Settings,
"auth.json" => ConfigFile::Auth,
"models.json" => ConfigFile::Models,
"keybindings.json" => ConfigFile::Keybindings,
"mcp.json" => ConfigFile::Mcp,
"mcp-auth.json" => ConfigFile::McpAuth,
other => panic!("unknown config fixture {other}"),
}
}
#[test]
fn sessions_round_trip_byte_identical() {
for path in fixtures("sessions").into_iter().chain(fixtures("legacy")) {
let text = fs::read_to_string(&path).unwrap();
assert!(
text.ends_with('\n'),
"{}: missing final newline",
path.display()
);
for (index, line) in text.split_terminator('\n').enumerate() {
let document: Value = serde_json::from_str(line).unwrap();
assert_same(
&json::to_string(&document).unwrap(),
line,
&format!("{}:{}", path.display(), index + 1),
);
}
}
}
#[test]
fn configs_round_trip_byte_identical() {
for path in fixtures("agent").into_iter().chain(fixtures("project")) {
let text = fs::read_to_string(&path).unwrap();
let document: Value = serde_json::from_str(&text).unwrap();
assert_same(
&config_kind(&path).render(&document).unwrap(),
&text,
&path.display().to_string(),
);
}
}
#[test]
fn session_views_cover_fixtures() {
for path in fixtures("sessions") {
let text = fs::read_to_string(&path).unwrap();
for (index, line) in text.split_terminator('\n').enumerate() {
let document: Value = serde_json::from_str(line).unwrap();
assert_covers::<FileEntry>(&document, &format!("{}:{}", path.display(), index + 1));
}
}
}
#[test]
fn typed_views_use_pi_key_order() {
let text = fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/pi/sessions/main.jsonl"),
)
.unwrap();
for (index, line) in text.split_terminator('\n').enumerate() {
let entry: FileEntry = serde_json::from_str(line).unwrap();
let path_dependent = match &entry {
FileEntry::Message(entry) => matches!(entry.message, Message::Assistant(_)),
FileEntry::Compaction(_) => true,
_ => false,
};
if !path_dependent {
assert_same(
&json::to_string(&entry).unwrap(),
line,
&format!("main.jsonl:{}", index + 1),
);
}
}
}
#[test]
fn config_views_cover_fixtures() {
for path in fixtures("agent").into_iter().chain(fixtures("project")) {
let document: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
let location = path.display().to_string();
match config_kind(&path) {
ConfigFile::Settings => assert_covers::<Settings>(&document, &location),
ConfigFile::Auth => assert_covers::<AuthFile>(&document, &location),
ConfigFile::Models => assert_covers::<ModelsConfig>(&document, &location),
ConfigFile::Keybindings | ConfigFile::Mcp | ConfigFile::McpAuth => {}
}
}
}
fn assert_same(actual: &str, expected: &str, location: &str) {
if actual != expected {
let at = actual
.bytes()
.zip(expected.bytes())
.take_while(|(a, b)| a == b)
.count();
let context = |s: &str| {
s.get(at.saturating_sub(40)..(at + 40).min(s.len()))
.unwrap_or("")
.to_owned()
};
panic!(
"{location}: differs at byte {at}\n expected: …{}…\n actual: …{}…",
context(expected),
context(actual)
);
}
}
fn assert_covers<T: DeserializeOwned + Serialize>(document: &Value, location: &str) {
let typed: T = serde_json::from_value(document.clone())
.unwrap_or_else(|err| panic!("{location}: does not decode: {err}"));
let encoded = serde_json::to_value(&typed).unwrap();
if let Some(path) = first_difference(&normalize(document), &normalize(&encoded), String::new())
{
panic!("{location}: typed view differs at `{path}`");
}
}
fn normalize(value: &Value) -> Value {
match value {
Value::Number(number) => serde_json::json!(number.as_f64().unwrap()),
Value::Array(items) => Value::Array(items.iter().map(normalize).collect()),
Value::Object(map) => {
Value::Object(map.iter().map(|(k, v)| (k.clone(), normalize(v))).collect())
}
other => other.clone(),
}
}
fn first_difference(expected: &Value, actual: &Value, path: String) -> Option<String> {
match (expected, actual) {
(Value::Object(a), Value::Object(b)) => a
.keys()
.chain(b.keys().filter(|key| !a.contains_key(*key)))
.find_map(|key| match (a.get(key), b.get(key)) {
(Some(x), Some(y)) => first_difference(x, y, format!("{path}.{key}")),
(Some(_), None) => Some(format!("{path}.{key} (dropped)")),
(None, _) => Some(format!("{path}.{key} (added)")),
}),
(Value::Array(a), Value::Array(b)) if a.len() == b.len() => a
.iter()
.zip(b)
.enumerate()
.find_map(|(i, (x, y))| first_difference(x, y, format!("{path}[{i}]"))),
_ if expected == actual => None,
_ => Some(format!("{path} ({expected} vs {actual})")),
}
}