The branch passes the checklist: seven commits, copied files unchanged, gate and audit green. Reading and probing found that AuditRecord and ToolCall accept unknown fields, that large Timestamps panic when formatted, and that the dependency-direction scripts miss table-form dependencies and pass when their inputs are missing. The last two families were gaps in the tasks, not only in the code. Tasks 08 and 09 carry the fixes, defined by an exhaustive unknown-field test, a bounded-Timestamp test and an extended gate-script self-test. All three were checked against the reference implementation and fail on the current branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
196 lines
5.8 KiB
Rust
196 lines
5.8 KiB
Rust
//! Tests for identifiers and primitive values. Do not edit: these define the required behaviour.
|
|
|
|
use proto::{CallId, DataClass, Epoch, Hash32, SessionId, Timestamp, ValueError};
|
|
|
|
#[test]
|
|
fn session_id_accepts_lowercase_digits_and_hyphen() {
|
|
for ok in ["a", "mm-thread-42", "0", "a-b-c", &"x".repeat(64)] {
|
|
assert_eq!(SessionId::new(ok).unwrap().as_str(), ok);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn session_id_rejects_everything_else() {
|
|
let too_long = "x".repeat(65);
|
|
for bad in [
|
|
"",
|
|
"A",
|
|
"a b",
|
|
"a/b",
|
|
"../etc",
|
|
"a.b",
|
|
"a_b",
|
|
"é",
|
|
"a\n",
|
|
too_long.as_str(),
|
|
] {
|
|
assert_eq!(
|
|
SessionId::new(bad),
|
|
Err(ValueError::SessionId),
|
|
"accepted {bad:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn session_id_json_is_a_plain_string_and_is_validated() {
|
|
let id = SessionId::new("mm-thread-42").unwrap();
|
|
assert_eq!(serde_json::to_string(&id).unwrap(), r#""mm-thread-42""#);
|
|
assert_eq!(
|
|
serde_json::from_str::<SessionId>(r#""mm-thread-42""#).unwrap(),
|
|
id
|
|
);
|
|
assert!(serde_json::from_str::<SessionId>(r#""../etc""#).is_err());
|
|
assert!(serde_json::from_str::<SessionId>("42").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn epoch_and_call_id_are_plain_numbers() {
|
|
assert_eq!(serde_json::to_string(&Epoch(3)).unwrap(), "3");
|
|
assert_eq!(
|
|
serde_json::to_string(&CallId(18446744073709551615)).unwrap(),
|
|
"18446744073709551615"
|
|
);
|
|
assert_eq!(serde_json::from_str::<CallId>("7").unwrap(), CallId(7));
|
|
assert!(serde_json::from_str::<CallId>("-1").is_err());
|
|
assert!(serde_json::from_str::<CallId>("1.5").is_err());
|
|
assert!(serde_json::from_str::<Epoch>("4294967296").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn hash32_hex_round_trip() {
|
|
let mut bytes = [0u8; 32];
|
|
for (i, b) in bytes.iter_mut().enumerate() {
|
|
*b = i as u8;
|
|
}
|
|
let h = Hash32::from_bytes(bytes);
|
|
let hex = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
|
assert_eq!(h.to_hex(), hex);
|
|
assert_eq!(Hash32::from_hex(hex).unwrap(), h);
|
|
assert_eq!(h.as_bytes(), &bytes);
|
|
assert_eq!(serde_json::to_string(&h).unwrap(), format!("\"{hex}\""));
|
|
assert_eq!(
|
|
serde_json::from_str::<Hash32>(&format!("\"{hex}\"")).unwrap(),
|
|
h
|
|
);
|
|
assert_eq!(Hash32::ZERO.to_hex(), "0".repeat(64));
|
|
}
|
|
|
|
#[test]
|
|
fn hash32_rejects_wrong_length_uppercase_and_non_hex() {
|
|
let upper = "000102030405060708090A0B0C0D0E0F101112131415161718191a1b1c1d1e1f";
|
|
let non_hex = "g00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
|
let non_ascii = format!("é{}", "0".repeat(62));
|
|
for bad in [
|
|
"",
|
|
"00",
|
|
&"0".repeat(63),
|
|
&"0".repeat(65),
|
|
upper,
|
|
non_hex,
|
|
non_ascii.as_str(),
|
|
] {
|
|
assert_eq!(
|
|
Hash32::from_hex(bad),
|
|
Err(ValueError::Hash32),
|
|
"accepted {bad:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_has_one_spelling() {
|
|
let t = Timestamp::from_unix_millis(1_789_632_300_000).unwrap();
|
|
assert_eq!(t.unix_millis(), 1_789_632_300_000);
|
|
assert_eq!(t.to_rfc3339(), "2026-09-17T08:05:00.000Z");
|
|
assert_eq!(Timestamp::parse("2026-09-17T08:05:00.000Z").unwrap(), t);
|
|
assert_eq!(
|
|
serde_json::to_string(&t).unwrap(),
|
|
r#""2026-09-17T08:05:00.000Z""#
|
|
);
|
|
assert_eq!(
|
|
Timestamp::from_unix_millis(0).unwrap().to_rfc3339(),
|
|
"1970-01-01T00:00:00.000Z"
|
|
);
|
|
assert_eq!(
|
|
Timestamp::from_unix_millis(1_789_632_300_007)
|
|
.unwrap()
|
|
.to_rfc3339(),
|
|
"2026-09-17T08:05:00.007Z"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_rejects_other_spellings() {
|
|
for bad in [
|
|
"2026-09-17T08:05:00Z",
|
|
"2026-09-17T08:05:00.0Z",
|
|
"2026-09-17T08:05:00.000000Z",
|
|
"2026-09-17 08:05:00.000Z",
|
|
"2026-09-17T08:05:00.000+00:00",
|
|
"2026-09-17t08:05:00.000z",
|
|
"2026-09-17",
|
|
"",
|
|
"now",
|
|
] {
|
|
assert_eq!(
|
|
Timestamp::parse(bad),
|
|
Err(ValueError::Timestamp),
|
|
"accepted {bad:?}"
|
|
);
|
|
assert!(serde_json::from_str::<Timestamp>(&format!("\"{bad}\"")).is_err());
|
|
}
|
|
assert!(serde_json::from_str::<Timestamp>("1789632300000").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_range_ends_with_year_9999() {
|
|
assert_eq!(Timestamp::MAX.unix_millis(), 253_402_300_799_999);
|
|
assert_eq!(Timestamp::MAX.to_rfc3339(), "9999-12-31T23:59:59.999Z");
|
|
assert_eq!(
|
|
Timestamp::parse("9999-12-31T23:59:59.999Z").unwrap(),
|
|
Timestamp::MAX
|
|
);
|
|
assert_eq!(
|
|
Timestamp::from_unix_millis(253_402_300_799_999).unwrap(),
|
|
Timestamp::MAX
|
|
);
|
|
// One millisecond later has no RFC 3339 spelling, so it must not become a Timestamp at all.
|
|
assert_eq!(
|
|
Timestamp::from_unix_millis(253_402_300_800_000),
|
|
Err(ValueError::Timestamp)
|
|
);
|
|
assert_eq!(
|
|
Timestamp::from_unix_millis(u64::MAX),
|
|
Err(ValueError::Timestamp)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_now_is_after_2026() {
|
|
assert!(Timestamp::now() > Timestamp::parse("2026-01-01T00:00:00.000Z").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn data_class_is_ordered_and_lowercase() {
|
|
assert!(DataClass::Public < DataClass::Private);
|
|
assert!(DataClass::Private < DataClass::Secret);
|
|
assert_eq!(DataClass::Private.max(DataClass::Secret), DataClass::Secret);
|
|
assert_eq!(
|
|
serde_json::to_string(&DataClass::Secret).unwrap(),
|
|
r#""secret""#
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<DataClass>(r#""public""#).unwrap(),
|
|
DataClass::Public
|
|
);
|
|
assert!(serde_json::from_str::<DataClass>(r#""Public""#).is_err());
|
|
assert!(serde_json::from_str::<DataClass>(r#""internal""#).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn value_error_is_a_std_error_with_a_message() {
|
|
let e: Box<dyn std::error::Error> = Box::new(ValueError::Hash32);
|
|
assert!(!e.to_string().is_empty());
|
|
}
|