Review M1: accept with two follow-up tasks
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>
This commit is contained in:
@@ -100,7 +100,7 @@ fn hash32_rejects_wrong_length_uppercase_and_non_hex() {
|
||||
|
||||
#[test]
|
||||
fn timestamp_has_one_spelling() {
|
||||
let t = Timestamp::from_unix_millis(1_789_632_300_000);
|
||||
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);
|
||||
@@ -109,11 +109,13 @@ fn timestamp_has_one_spelling() {
|
||||
r#""2026-09-17T08:05:00.000Z""#
|
||||
);
|
||||
assert_eq!(
|
||||
Timestamp::from_unix_millis(0).to_rfc3339(),
|
||||
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).to_rfc3339(),
|
||||
Timestamp::from_unix_millis(1_789_632_300_007)
|
||||
.unwrap()
|
||||
.to_rfc3339(),
|
||||
"2026-09-17T08:05:00.007Z"
|
||||
);
|
||||
}
|
||||
@@ -141,6 +143,29 @@ fn timestamp_rejects_other_spellings() {
|
||||
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());
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Every JSON object in every fixture must reject an unknown key. Do not edit.
|
||||
//!
|
||||
//! The other test files check unknown fields in a few hand-picked places. This one checks all of
|
||||
//! them: it walks each fixture, adds one unknown key to one object at a time, at every depth, and
|
||||
//! requires that the result no longer decodes.
|
||||
|
||||
use proto::{AuditRecord, Envelope, Grant, LogRecord};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Every copy of `value` that has exactly one extra key in exactly one object.
|
||||
fn with_one_unknown_key(value: &Value) -> Vec<Value> {
|
||||
let mut out = Vec::new();
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let mut extended = map.clone();
|
||||
extended.insert("zz_unknown".to_string(), Value::Bool(true));
|
||||
out.push(Value::Object(extended));
|
||||
for (key, child) in map {
|
||||
for changed in with_one_unknown_key(child) {
|
||||
let mut copy = map.clone();
|
||||
copy.insert(key.clone(), changed);
|
||||
out.push(Value::Object(copy));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for (i, child) in items.iter().enumerate() {
|
||||
for changed in with_one_unknown_key(child) {
|
||||
let mut copy = items.clone();
|
||||
copy[i] = changed;
|
||||
out.push(Value::Array(copy));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Returns how many variations were tried, so callers can check the walk reached nested objects.
|
||||
fn check<T: DeserializeOwned>(what: &str, text: &str) -> usize {
|
||||
let value: Value = serde_json::from_str(text).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert!(
|
||||
serde_json::from_value::<T>(value.clone()).is_ok(),
|
||||
"{what}: fixture must decode"
|
||||
);
|
||||
let variations = with_one_unknown_key(&value);
|
||||
for changed in &variations {
|
||||
assert!(
|
||||
serde_json::from_value::<T>(changed.clone()).is_err(),
|
||||
"{what}: accepted an unknown key: {changed}"
|
||||
);
|
||||
}
|
||||
variations.len()
|
||||
}
|
||||
|
||||
fn fixture(path: &str) -> String {
|
||||
let full = format!("{}/tests/fixtures/{path}", env!("CARGO_MANIFEST_DIR"));
|
||||
std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{full}: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelopes_reject_unknown_keys_at_every_depth() {
|
||||
for name in [
|
||||
"tool_request.json",
|
||||
"tool_response_pending.json",
|
||||
"tool_response_result.json",
|
||||
"tool_response_failed.json",
|
||||
"tool_response_denied.json",
|
||||
"error.json",
|
||||
] {
|
||||
// Envelope, msg and body: three objects.
|
||||
assert_eq!(
|
||||
check::<Envelope>(name, &fixture(&format!("wire/{name}"))),
|
||||
3,
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_records_reject_unknown_keys_at_every_depth() {
|
||||
for (i, line) in fixture("records/audit.jsonl").lines().enumerate() {
|
||||
// The record and its decision: two objects.
|
||||
assert_eq!(
|
||||
check::<AuditRecord>(&format!("audit.jsonl:{}", i + 1), line),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_records_reject_unknown_keys_at_every_depth() {
|
||||
let mut tried = 0;
|
||||
for (i, line) in fixture("records/session.jsonl").lines().enumerate() {
|
||||
tried += check::<LogRecord>(&format!("session.jsonl:{}", i + 1), line);
|
||||
}
|
||||
// Seven records, plus the one tool call inside the first assistant record.
|
||||
assert_eq!(tried, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grants_reject_unknown_keys_at_every_depth() {
|
||||
let grant: Grant = toml::from_str(&fixture("grant/full.toml")).unwrap();
|
||||
let text = serde_json::to_string(&grant).unwrap();
|
||||
// The grant and its constraints: two objects.
|
||||
assert_eq!(check::<Grant>("full.toml as JSON", &text), 2);
|
||||
}
|
||||
@@ -63,6 +63,15 @@ tree "$tmp/c-dev"; printf '\n[dev-dependencies]\nbrokerd.workspace = true\n' >>
|
||||
expect fail "role dev-depends on another role" check-crate-deps.sh "$tmp/c-dev"
|
||||
tree "$tmp/c-proto"; manifest "$tmp/c-proto/crates/proto" proto 'loopd.workspace = true'
|
||||
expect fail "proto depends on a role" check-crate-deps.sh "$tmp/c-proto"
|
||||
# A dependency can also be written as its own table: [dependencies.NAME]
|
||||
tree "$tmp/c-sect"; printf '\n[dependencies.brokerd]\npath = "../brokerd"\n' >> "$tmp/c-sect/crates/loopd/Cargo.toml"
|
||||
expect fail "role depends on another role, written as a table" check-crate-deps.sh "$tmp/c-sect"
|
||||
tree "$tmp/c-sectw"; printf '\n[dev-dependencies.brokerd]\nworkspace = true\n' >> "$tmp/c-sectw/crates/loopd/Cargo.toml"
|
||||
expect fail "role dev-depends on another role, written as a table" check-crate-deps.sh "$tmp/c-sectw"
|
||||
tree "$tmp/c-dot"; manifest "$tmp/c-dot/crates/loopd" loopd 'proto.workspace = true' 'brokerd.path = "../brokerd"'
|
||||
expect fail "role depends on another role, written with a dotted key" check-crate-deps.sh "$tmp/c-dot"
|
||||
tree "$tmp/c-sectok"; manifest "$tmp/c-sectok/crates/loopd" loopd; printf '\n[dependencies.proto]\nworkspace = true\n' >> "$tmp/c-sectok/crates/loopd/Cargo.toml"
|
||||
expect pass "role depends on proto, written as a table" check-crate-deps.sh "$tmp/c-sectok"
|
||||
|
||||
# check-dep-docs.sh
|
||||
tree "$tmp/d-ok"
|
||||
@@ -73,6 +82,18 @@ tree "$tmp/d-prose"; printf 'rand = "0.9"\n' >> "$tmp/d-prose/Cargo.toml"; print
|
||||
expect fail "a mention in prose is not a table row" check-dep-docs.sh "$tmp/d-prose"
|
||||
tree "$tmp/d-loose"; manifest "$tmp/d-loose/crates/loopd" loopd 'proto.workspace = true' 'rand = "0.9"'
|
||||
expect fail "crate declares a dependency outside the workspace table" check-dep-docs.sh "$tmp/d-loose"
|
||||
tree "$tmp/d-sect"; printf '\n[dependencies.rand]\nversion = "0.9"\n' >> "$tmp/d-sect/crates/loopd/Cargo.toml"
|
||||
expect fail "table-form dependency without workspace = true" check-dep-docs.sh "$tmp/d-sect"
|
||||
tree "$tmp/d-sectok"; printf '\n[dependencies.serde]\nworkspace = true\nfeatures = ["derive"]\n' >> "$tmp/d-sectok/crates/loopd/Cargo.toml"
|
||||
expect pass "table-form dependency with workspace = true" check-dep-docs.sh "$tmp/d-sectok"
|
||||
|
||||
# A check that cannot find what it checks must fail, not pass.
|
||||
mkdir -p "$tmp/empty"
|
||||
expect fail "check-lines without a crates directory" check-lines.sh "$tmp/empty"
|
||||
expect fail "check-crate-deps without a crates directory" check-crate-deps.sh "$tmp/empty"
|
||||
expect fail "check-dep-docs without a crates directory" check-dep-docs.sh "$tmp/empty"
|
||||
tree "$tmp/d-nodoc"; rm "$tmp/d-nodoc/docs/dependencies.md"
|
||||
expect fail "check-dep-docs without docs/dependencies.md" check-dep-docs.sh "$tmp/d-nodoc"
|
||||
|
||||
if [ "$fails" -ne 0 ]; then
|
||||
echo "test-gate-scripts: $fails failure(s)" >&2
|
||||
|
||||
Reference in New Issue
Block a user