#![allow(clippy::panic, clippy::unnecessary_debug_formatting)]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::{Path, PathBuf};
use meerkat_core::{SESSION_METADATA_SCHEMA_VERSION, SESSION_VERSION};
use meerkat_session::migrations::{
STORED_INPUT_STATE_VERSION, SessionMigrationError, migrate_input_state_value,
migrate_session_value,
};
use serde_json::{Value, json};
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("pre_wave_b")
}
fn load_fixture(name: &str) -> Value {
let path = fixture_dir().join(format!("{name}.json"));
let bytes = std::fs::read(&path)
.unwrap_or_else(|err| panic!("fixture {name} missing at {path:?}: {err}"));
serde_json::from_slice(&bytes).unwrap_or_else(|err| panic!("fixture {name} parse error: {err}"))
}
fn migrate_metadata_scenario(name: &str) -> Result<meerkat_core::Session, SessionMigrationError> {
let raw = load_fixture(name);
let metadata_root = raw
.as_object()
.and_then(|obj| obj.get("session_metadata").cloned())
.unwrap_or(Value::Null);
let session_llm_identity = raw
.as_object()
.and_then(|obj| obj.get("session_llm_identity").cloned());
let id = raw
.get("id")
.and_then(Value::as_str)
.unwrap_or("00000000-0000-0000-0000-000000000001")
.to_string();
let mut envelope = json!({
"id": id,
"messages": [],
"created_at": { "secs_since_epoch": 0, "nanos_since_epoch": 0 },
"updated_at": { "secs_since_epoch": 0, "nanos_since_epoch": 0 },
"metadata": {
"session_metadata": metadata_root,
},
});
if let Some(ident) = session_llm_identity {
envelope
.as_object_mut()
.unwrap()
.insert("session_llm_identity_scenario".to_string(), ident);
}
migrate_session_value(envelope)
}
#[test]
fn fixture_01_session_empty_metadata() {
let envelope = json!({
"id": "00000000-0000-0000-0000-000000000001",
"messages": [],
"created_at": { "secs_since_epoch": 0, "nanos_since_epoch": 0 },
"updated_at": { "secs_since_epoch": 0, "nanos_since_epoch": 0 },
});
let session = migrate_session_value(envelope).expect("empty metadata must migrate cleanly");
assert_eq!(session.version(), SESSION_VERSION);
assert!(
session.metadata().get("session_metadata").is_none(),
"migrator must not synthesize SessionMetadata when none was persisted"
);
}
#[test]
fn fixture_02_session_provider_params_openai() {
let session =
migrate_metadata_scenario("session_provider_params_openai").expect("must migrate");
let metadata_json = session
.metadata()
.get("session_metadata")
.expect("session_metadata must be present after migration");
let params = metadata_json
.get("provider_params")
.expect("provider_params preserved");
assert_eq!(params.get("temperature").and_then(Value::as_f64), Some(0.2));
assert_eq!(
params.get("reasoning").and_then(Value::as_str),
Some("silent")
);
assert_eq!(
params.get("encrypted_content").and_then(Value::as_str),
Some("Zm9v")
);
assert_eq!(
metadata_json.get("schema_version").and_then(Value::as_u64),
Some(u64::from(SESSION_METADATA_SCHEMA_VERSION))
);
}
#[test]
fn fixture_03_session_provider_params_anthropic_signature() {
let session = migrate_metadata_scenario("session_provider_params_anthropic_signature")
.expect("must migrate");
let params = session
.metadata()
.get("session_metadata")
.and_then(|m| m.get("provider_params"))
.expect("provider_params preserved");
assert_eq!(
params.get("signature").and_then(Value::as_str),
Some("abc123")
);
}
#[test]
fn fixture_04_anthropic_thinking_canary() {
let session = migrate_metadata_scenario("session_provider_params_anthropic_thinking")
.expect("must migrate");
let params = session
.metadata()
.get("session_metadata")
.and_then(|m| m.get("provider_params"))
.expect("provider_params preserved");
let thinking = params
.get("thinking")
.expect("thinking bag MUST survive — silent-drop canary");
assert_eq!(
thinking.get("type").and_then(Value::as_str),
Some("enabled"),
"thinking.type dropped by migration — regression of silent-drop canary"
);
assert_eq!(
thinking.get("budget_tokens").and_then(Value::as_u64),
Some(32000),
"thinking.budget_tokens dropped — regression of silent-drop canary"
);
let reserialized = serde_json::to_value(&session).unwrap();
let again = migrate_session_value(reserialized).expect("idempotent on v2 blob");
let thinking_again = again
.metadata()
.get("session_metadata")
.and_then(|m| m.get("provider_params"))
.and_then(|p| p.get("thinking"))
.expect("thinking survives second migration pass");
assert_eq!(
thinking_again.get("budget_tokens").and_then(Value::as_u64),
Some(32000)
);
}
#[test]
fn fixture_05_session_provider_params_unknown_scalar() {
let session =
migrate_metadata_scenario("session_provider_params_unknown").expect("must migrate");
let params = session
.metadata()
.get("session_metadata")
.and_then(|m| m.get("provider_params"))
.expect("provider_params preserved");
assert_eq!(params.as_u64(), Some(42));
}
#[test]
fn fixture_06_auth_binding_clean_v0_rename() {
let session =
migrate_metadata_scenario("session_auth_binding_slug_valid").expect("must migrate");
let cref = session
.metadata()
.get("session_metadata")
.and_then(|m| m.get("auth_binding"))
.expect("auth_binding preserved");
assert_eq!(cref.get("realm").and_then(Value::as_str), Some("dev"));
assert_eq!(
cref.get("binding").and_then(Value::as_str),
Some("default_openai")
);
assert!(cref.get("realm_id").is_none(), "legacy key must be removed");
assert!(
cref.get("binding_id").is_none(),
"legacy key must be removed"
);
}
#[test]
fn fixture_07_auth_binding_invalid_slug_slugified() {
let result = migrate_metadata_scenario("session_auth_binding_slug_invalid");
match result {
Err(SessionMigrationError::Partial(partial)) => {
let cref = partial
.session
.metadata()
.get("session_metadata")
.and_then(|m| m.get("auth_binding"))
.expect("auth_binding coerced to valid slug");
assert_eq!(
cref.get("realm").and_then(Value::as_str),
Some("dev_mode"),
"realm_id `dev mode` must slugify to `dev_mode`"
);
let salvaged = partial
.legacy
.get("legacy_auth_binding_session_metadata")
.expect("legacy payload retained");
assert_eq!(
salvaged.get("realm_id").and_then(Value::as_str),
Some("dev mode"),
"original `dev mode` must survive under legacy bag"
);
}
Ok(_) => panic!("invalid slug must return Partial, not Ok"),
Err(other) => panic!("unexpected migration error: {other:?}"),
}
}
#[test]
fn fixture_08_hot_swap_identity_mixed() {
let session =
migrate_metadata_scenario("session_hot_swap_identity_mixed").expect("must migrate");
let cref = session
.metadata()
.get("session_metadata")
.and_then(|m| m.get("auth_binding"))
.expect("auth_binding preserved");
assert_eq!(cref.get("realm").and_then(Value::as_str), Some("prod"));
assert_eq!(
cref.get("binding").and_then(Value::as_str),
Some("openai_main")
);
}
#[test]
fn fixture_09_input_state_full_turn_metadata() {
let raw = load_fixture("input_state_prompt_full_turn_metadata");
let migrated = migrate_input_state_value(raw).expect("input state migrates");
let tm = migrated
.get("persisted_input")
.and_then(|p| p.get("Prompt"))
.and_then(|p| p.get("turn_metadata"))
.expect("turn_metadata survives migration");
assert_eq!(
tm.get("provider_params")
.and_then(|p| p.get("temperature"))
.and_then(Value::as_f64),
Some(0.7)
);
let instr = tm
.get("additional_instructions")
.and_then(Value::as_array)
.expect("additional_instructions preserved");
assert_eq!(instr.len(), 2);
assert_eq!(instr[0].as_str(), Some("foo"));
assert_eq!(
migrated
.get("stored_input_state_version")
.and_then(Value::as_u64),
Some(u64::from(STORED_INPUT_STATE_VERSION))
);
}
#[test]
fn fixture_10_input_state_continuation_minimal() {
let raw = load_fixture("input_state_continuation_minimal");
let migrated = migrate_input_state_value(raw).expect("input state migrates");
assert!(
migrated
.get("persisted_input")
.and_then(|p| p.get("Continuation"))
.and_then(|c| c.get("turn_metadata"))
.is_none(),
"migration must not fabricate turn_metadata"
);
assert_eq!(
migrated
.get("stored_input_state_version")
.and_then(Value::as_u64),
Some(u64::from(STORED_INPUT_STATE_VERSION))
);
}
#[test]
fn fixture_11_input_state_provider_unknown_string() {
let raw = load_fixture("input_state_provider_unknown_string");
let migrated = migrate_input_state_value(raw).expect("input state migrates");
let provider = migrated
.get("persisted_input")
.and_then(|p| p.get("Prompt"))
.and_then(|p| p.get("turn_metadata"))
.and_then(|tm| tm.get("provider"))
.and_then(Value::as_str);
assert_eq!(
provider,
Some("retired_backend_v0"),
"retired provider string preserved — downstream Provider::deserialize \
surfaces the typed error without silent-dropping the legacy value"
);
}
#[test]
fn fixture_12_runtime_session_snapshot_drift() {
let raw = load_fixture("runtime_session_snapshot_drift");
let snapshot = raw
.get("snapshot")
.cloned()
.expect("fixture contains a `snapshot` wrapper");
let migrated =
migrate_session_value(snapshot).expect("runtime snapshot must round-trip via migration");
let thinking = migrated
.metadata()
.get("session_metadata")
.and_then(|m| m.get("provider_params"))
.and_then(|p| p.get("thinking"))
.expect("thinking survives runtime snapshot migration");
assert_eq!(
thinking.get("budget_tokens").and_then(Value::as_u64),
Some(32000)
);
}