use std::collections::BTreeSet;
use proptest::prelude::*;
use serde_json::{Map, Value, json};
use sha2::{Digest, Sha256};
use super::{
DERIVE_EPOCH_MS, HandleResolution, SessionSource, build_handle_tags, build_mint_back_text,
build_structured_mint_back, derive_session_id, extract_handle, is_valid_session_id, ksuid,
new_event_id, new_session_id, resolve_handles, session_from_hook_value,
};
use crate::constants::{AGENT_ID_PARAM, SESSION_ID_PARAM, SESSION_START_SENTINEL};
use crate::test_support::rules::{base62_bytes, check_cut};
use crate::test_support::strategies::{edge_char, edge_string};
use crate::test_support::{assert_same_json_text, config, vectors};
use crate::wire;
const KINDS: [&str; 9] = [
"derive",
"valid",
"extract",
"resolve",
"mint-back",
"structured",
"tags",
"hook-value",
"lowercase",
];
const MINTED: &str = "<minted>";
const BASE62: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const SOURCES: [SessionSource; 5] = [
SessionSource::Hook,
SessionSource::Supplied,
SessionSource::Minted,
SessionSource::Invalid,
SessionSource::Foreign,
];
fn source_from_json(text: &str) -> SessionSource {
SOURCES
.into_iter()
.find(|source| source.as_str() == text)
.unwrap_or_else(|| panic!("no session source {text:?}"))
}
fn resolution_from_json(value: &Value) -> HandleResolution {
let field = |name: &str| {
value
.get(name)
.unwrap_or_else(|| panic!("resolution {value} has no {name}"))
};
let agent_id = value.get("agentId").map(|agent| {
assert_eq!(
value.get("agentSource"),
Some(&json!("supplied")),
"{value}"
);
agent.as_str().unwrap().to_owned()
});
HandleResolution {
session_id: field("sessionId").as_str().unwrap().to_owned(),
session_source: source_from_json(field("sessionSource").as_str().unwrap()),
hook_mode: field("hookMode").as_bool().unwrap(),
agent_id,
}
}
fn resolution_to_json(res: &HandleResolution) -> Value {
let mut fixture = Map::new();
fixture.insert("sessionId".into(), json!(res.session_id));
fixture.insert("sessionSource".into(), json!(res.session_source.as_str()));
fixture.insert("hookMode".into(), json!(res.hook_mode));
if let Some(agent) = &res.agent_id {
fixture.insert("agentId".into(), json!(agent));
fixture.insert("agentSource".into(), json!("supplied"));
}
Value::Object(fixture)
}
fn mask(session_id: &str, source: SessionSource) -> Value {
if source == SessionSource::Minted {
assert!(is_valid_session_id(session_id), "minted {session_id:?}");
json!(MINTED)
} else {
json!(session_id)
}
}
fn masked_resolution(res: &HandleResolution) -> Value {
let mut fixture = resolution_to_json(res);
fixture["sessionId"] = mask(&res.session_id, res.session_source);
fixture
}
fn args_of(input: &Value) -> Option<&Map<String, Value>> {
input.get("args").and_then(Value::as_object)
}
fn optional_str<'a>(input: &'a Value, field: &str) -> Option<&'a str> {
match input.get(field) {
None | Some(Value::Null) => None,
Some(value) => Some(
value
.as_str()
.unwrap_or_else(|| panic!("{field} of {input} is not a string")),
),
}
}
fn flag(input: &Value, field: &str) -> bool {
input[field]
.as_bool()
.unwrap_or_else(|| panic!("{field} of {input} is not a boolean"))
}
fn check_case(case: &Map<String, Value>) -> &'static str {
let name = vectors::name(case);
let kind = KINDS
.into_iter()
.find(|kind| {
name.strip_prefix(kind)
.is_some_and(|rest| rest.starts_with(':'))
})
.unwrap_or_else(|| panic!("{name}: unknown kind"));
let input = case.get("input").unwrap_or(&Value::Null);
let output = &case["output"];
match kind {
"derive" => {
let id = input["id"].as_str().unwrap();
let actual = derive_session_id(id, optional_str(input, "projectId"));
assert_same_json_text(&json!(actual), output, name);
}
"valid" => {
let value = input.as_str().unwrap();
assert_same_json_text(&json!(is_valid_session_id(value)), output, name);
}
"extract" => {
let param = input["name"].as_str().unwrap();
let actual = extract_handle(args_of(input), param);
assert_same_json_text(&json!(actual), output, name);
}
"resolve" => {
let resolve = || {
resolve_handles(
args_of(input),
flag(input, "hookMode"),
flag(input, "agentTracking"),
flag(input, "sessionParamIsOurs"),
)
.unwrap_or_else(|| panic!("{name}: minting failed"))
};
let res = resolve();
assert_same_json_text(&masked_resolution(&res), output, name);
if res.session_source == SessionSource::Minted {
let again = resolve();
assert_ne!(res.session_id, again.session_id, "{name}: one ID per mint");
}
}
"mint-back" => {
let actual = build_mint_back_text(&resolution_from_json(input));
assert_same_json_text(&json!(actual), output, name);
}
"structured" => {
let actual = build_structured_mint_back(&resolution_from_json(input));
assert_same_json_text(&json!(actual), output, name);
}
"tags" => {
let res = resolution_from_json(&input["res"]);
let actual = build_handle_tags(&res, optional_str(input, "protocolVersion"));
assert_same_json_text(&Value::Object(actual), output, name);
}
"hook-value" => {
let session = || {
session_from_hook_value(
optional_str(input, "value"),
optional_str(input, "projectId"),
)
.unwrap_or_else(|| panic!("{name}: minting failed"))
};
let (session_id, source) = session();
let actual = json!({
"sessionId": mask(&session_id, source),
"sessionSource": source.as_str(),
});
assert_same_json_text(&actual, output, name);
if source == SessionSource::Minted {
assert_ne!(session_id, session().0, "{name}: one ID per mint");
}
}
"lowercase" => check_lowercase(case),
_ => unreachable!("{name}: kind {kind:?} has no check"),
}
kind
}
fn check_lowercase(case: &Map<String, Value>) {
let name = vectors::name(case);
let letters = case["output"]["letters"]
.as_object()
.unwrap_or_else(|| panic!("{name}: output.letters is not an object"));
let sentinel: BTreeSet<char> = SESSION_START_SENTINEL.chars().collect();
assert_eq!(
letters.keys().cloned().collect::<BTreeSet<_>>(),
sentinel.iter().map(char::to_string).collect(),
"{name}: one entry per letter of the sentinel"
);
for (letter, chars) in letters {
let upper = letter.to_ascii_uppercase();
let listed: BTreeSet<String> = chars
.as_array()
.unwrap_or_else(|| panic!("{name}: letters.{letter} is not an array"))
.iter()
.map(|c| {
c.as_str()
.unwrap_or_else(|| panic!("{name}: {c} is not a string"))
.to_owned()
})
.collect();
assert_eq!(
listed,
BTreeSet::from([letter.clone(), upper]),
"{name}: only the ASCII cases of {letter} lowercase to it"
);
for unit in (0_u32..=0xFFFF).filter_map(char::from_u32) {
let text = unit.to_string();
assert_eq!(
text.eq_ignore_ascii_case(letter),
listed.contains(&text),
"{name}: U+{:04X} against {letter}",
u32::from(unit)
);
}
}
for (character, lower) in case["output"]["expanding"]
.as_object()
.unwrap_or_else(|| panic!("{name}: output.expanding is not an object"))
{
let lower = lower
.as_str()
.unwrap_or_else(|| panic!("{name}: expanding.{character} is not a string"));
assert!(
!SESSION_START_SENTINEL.contains(lower),
"{name}: {character:?} lowercases to {lower:?}, a part of the sentinel"
);
}
}
#[test]
fn fixture_cases_hold() {
let seen: BTreeSet<&str> = vectors::cases("handles").iter().map(check_case).collect();
assert_eq!(
seen,
BTreeSet::from(KINDS),
"handles.json must have cases of every kind"
);
}
#[test]
fn pattern_is_the_valid_shape_plus_the_sentinel() {
let cases = vectors::cases("handles");
let valid: Vec<_> = cases
.iter()
.filter(|case| vectors::name(case).starts_with("valid:"))
.collect();
for case in &valid {
let name = vectors::name(case);
let input = case["input"].as_str().unwrap();
let expected = is_valid_session_id(input) || input == SESSION_START_SENTINEL;
assert_eq!(case["pattern"].as_bool(), Some(expected), "{name}");
}
let contract = valid
.iter()
.filter(|case| vectors::name(case).starts_with("valid:contract:"))
.count();
assert_eq!(contract, 5, "the five inputs of the value-contract test");
}
fn has_session_shape(id: &str) -> bool {
id.strip_prefix("ses_")
.is_some_and(|body| body.chars().count() == 27 && body.chars().all(|c| BASE62.contains(c)))
}
#[test]
fn new_ids_have_the_shape() {
let first = new_session_id().unwrap();
let second = new_session_id().unwrap();
for id in [&first, &second] {
assert!(has_session_shape(id), "{id}");
assert!(is_valid_session_id(id), "{id}");
}
assert_ne!(first, second);
let derived = derive_session_id("anything", Some("proj_1"));
assert!(has_session_shape(&derived), "{derived}");
assert!(is_valid_session_id(&derived), "{derived}");
let event = new_event_id().unwrap();
let body = event.strip_prefix("evt_").unwrap();
assert_eq!(body.chars().count(), 27, "{event}");
assert!(body.chars().all(|c| BASE62.contains(c)), "{event}");
assert_ne!(event, new_event_id().unwrap());
}
fn session_shaped() -> impl Strategy<Value = String> + Clone {
let prefix = prop::sample::select(vec![
"ses_", "ses_", "ses_", "SES_", "Ses_", "ses", "ses-", "evt_", "", " ses_", "sess_",
]);
let digit = prop::sample::select(BASE62.chars().collect::<Vec<_>>());
let body = prop::collection::vec(digit, 25..=29).prop_map(String::from_iter);
let edit = proptest::option::of((edge_char(), 0..30_usize, any::<bool>()));
let tail = prop::sample::select(vec![
"", "", "", "\n", "\r", " ", "\u{85}", "\u{FEFF}", "\u{2028}",
]);
(prefix, body, edit, tail).prop_map(|(prefix, mut body, edit, tail)| {
if let Some((c, at, replace)) = edit {
let at = at.min(body.len());
if replace && at < body.len() {
body.replace_range(at..=at, c.encode_utf8(&mut [0; 4]));
} else {
body.insert(at, c);
}
}
format!("{prefix}{body}{tail}")
})
}
fn padding() -> impl Strategy<Value = String> + Clone {
let c = prop::sample::select(vec![
' ', '\t', '\n', '\r', '\u{A0}', '\u{3000}', '\u{FEFF}', '\u{85}', '\u{180E}', '\u{200B}',
]);
prop::collection::vec(c, 0..=2).prop_map(String::from_iter)
}
fn start_spelling() -> impl Strategy<Value = String> + Clone {
let letter = |options: &[char]| prop::sample::select(options.to_vec());
let word = (
letter(&['s', 'S', 's', 'S', 'Å¿', '\u{FF33}', '\u{A731}']),
letter(&['t', 'T', 't', 'T', '\u{FF54}']),
letter(&['a', 'A', 'a', 'A', '\u{FF21}']),
letter(&['r', 'R']),
letter(&['t', 'T']),
)
.prop_map(|(s, t, a, r, t2)| String::from_iter([s, t, a, r, t2]));
let affix = prop::sample::select(vec!["", "", "", "", "re", "s", " now"]);
(padding(), affix.clone(), word, affix, padding())
.prop_map(|(head, before, word, after, tail)| format!("{head}{before}{word}{after}{tail}"))
}
fn padded_valid_id() -> impl Strategy<Value = String> + Clone {
let digit = prop::sample::select(BASE62.chars().collect::<Vec<_>>());
let body = prop::collection::vec(digit, 27).prop_map(String::from_iter);
(padding(), body, padding()).prop_map(|(head, body, tail)| format!("{head}ses_{body}{tail}"))
}
fn handle_value() -> impl Strategy<Value = Option<Value>> + Clone {
let other = prop::sample::select(vec![
json!(null),
json!(42),
json!(true),
json!(["start"]),
json!({"a": "start"}),
]);
prop_oneof![
1 => Just(None),
3 => edge_string(8).prop_map(|s| Some(json!(s))),
3 => start_spelling().prop_map(|s| Some(json!(s))),
2 => padded_valid_id().prop_map(|s| Some(json!(s))),
1 => other.prop_map(Some),
]
}
fn handle_args() -> impl Strategy<Value = Option<Map<String, Value>>> + Clone {
let args = (
handle_value(),
handle_value(),
proptest::option::of(edge_string(4)),
)
.prop_map(|(session, agent, other)| {
let mut args = Map::new();
if let Some(other) = other {
args.insert("other".into(), json!(other));
}
if let Some(agent) = agent {
args.insert(AGENT_ID_PARAM.into(), agent);
}
if let Some(session) = session {
args.insert(SESSION_ID_PARAM.into(), session);
}
args
});
prop_oneof![1 => Just(None), 9 => args.prop_map(Some)]
}
fn tag_agent_id() -> impl Strategy<Value = String> + Clone {
prop_oneof![
edge_string(260),
(197_usize..=201, edge_string(3)).prop_map(|(n, tail)| "a".repeat(n) + &tail),
]
}
#[test]
fn every_kind_matches_handles_cases() {
let seen: BTreeSet<&str> = vectors::cases("handles_cases")
.iter()
.map(check_case)
.collect();
assert_eq!(
seen,
BTreeSet::from(["derive", "valid", "extract", "resolve", "tags"]),
"handles_cases.json has cases of each kind"
);
}
proptest! {
#![proptest_config(config())]
#[test]
fn derive_holds_the_hash_of_the_id_and_the_project(
id in edge_string(40),
project in prop_oneof![
1 => Just(None),
1 => Just(Some(String::new())),
3 => edge_string(10).prop_map(Some),
],
) {
let derived = derive_session_id(&id, project.as_deref());
prop_assert!(has_session_shape(&derived), "{}", derived);
let hashed = match project.as_deref() {
Some(project) if !project.is_empty() => format!("{id}:{project}"),
_ => id.clone(),
};
let hash = Sha256::digest(hashed.as_bytes());
let offset = u64::from(u32::from_be_bytes(hash[..4].try_into().unwrap()));
let seconds = u32::try_from((DERIVE_EPOCH_MS + offset - ksuid::EPOCH_MS) / 1_000).unwrap();
let bytes = base62_bytes(&derived["ses_".len()..]);
prop_assert_eq!(&bytes[..4], &seconds.to_be_bytes()[..]);
prop_assert_eq!(&bytes[4..], &hash[4..20]);
}
#[test]
fn is_valid_is_the_session_id_shape(value in prop_oneof![4 => session_shaped(), 1 => edge_string(30)]) {
prop_assert_eq!(is_valid_session_id(&value), has_session_shape(&value), "{:?}", value);
}
#[test]
fn extract_and_resolve_follow_the_session_rules(
args in handle_args(),
hook_mode in any::<bool>(),
agent_tracking in any::<bool>(),
session_param_is_ours in any::<bool>(),
) {
let handle = |param: &str| {
args.as_ref()
.and_then(|args| args.get(param))
.and_then(Value::as_str)
.map(wire::trim)
.filter(|text| !text.is_empty())
.map(str::to_owned)
};
for param in [SESSION_ID_PARAM, AGENT_ID_PARAM] {
prop_assert_eq!(extract_handle(args.as_ref(), param), handle(param), "{}", param);
}
let res = resolve_handles(args.as_ref(), hook_mode, agent_tracking, session_param_is_ours)
.unwrap();
prop_assert_eq!(res.hook_mode, hook_mode);
prop_assert_eq!(res.agent_id.clone(), handle(AGENT_ID_PARAM).filter(|_| agent_tracking));
let session = handle(SESSION_ID_PARAM);
let expected = if hook_mode {
Some((String::new(), SessionSource::Hook))
} else if !session_param_is_ours {
Some((String::new(), SessionSource::Foreign))
} else if session.as_ref().is_none_or(|value| value.eq_ignore_ascii_case("start")) {
None
} else if session.as_deref().is_some_and(has_session_shape) {
session.map(|value| (value, SessionSource::Supplied))
} else {
Some((String::new(), SessionSource::Invalid))
};
if let Some(expected) = expected {
prop_assert_eq!((res.session_id, res.session_source), expected);
} else {
prop_assert_eq!(res.session_source, SessionSource::Minted);
prop_assert!(has_session_shape(&res.session_id), "{}", res.session_id);
}
}
#[test]
fn handle_tags_hold_the_source_the_cut_agent_and_the_version(
agent_id in proptest::option::of(tag_agent_id()),
source in prop::sample::select(SOURCES.to_vec()),
hook_mode in any::<bool>(),
protocol_version in proptest::option::of(prop_oneof![
Just(String::new()),
Just("2025-11-25".to_owned()),
edge_string(6),
]),
) {
let res = HandleResolution {
session_id: String::new(),
session_source: source,
hook_mode,
agent_id: agent_id.clone(),
};
let tags = build_handle_tags(&res, protocol_version.as_deref());
prop_assert_eq!(&tags["agentcat_session_id_source"], source.as_str());
let mut keys = vec!["agentcat_session_id_source"];
if let Some(agent) = agent_id.filter(|agent| !agent.is_empty()) {
let flat: String = agent.chars().map(|c| if c == '\r' || c == '\n' { ' ' } else { c }).collect();
let tag = tags.get("agentcat_agent_id").and_then(Value::as_str);
check_cut(&flat, tag.unwrap_or_default(), 200)?;
prop_assert_eq!(tags.get("agentcat_agent_id_source").cloned(), Some(json!("supplied")));
keys.extend(["agentcat_agent_id", "agentcat_agent_id_source"]);
}
if let Some(version) = protocol_version.filter(|version| !version.is_empty()) {
prop_assert_eq!(tags.get("agentcat_protocol_version").cloned(), Some(json!(version)));
keys.push("agentcat_protocol_version");
}
prop_assert_eq!(tags.keys().map(String::as_str).collect::<Vec<_>>(), keys);
}
}