M3a task 01: copy a strict.rs of its own

The strict.rs in files/ is the merged one and walks task 02's new wire
fixtures, so task 01's gate could not pass; the first run stopped on it.
Task 01 now copies area A's version, checked to give 3 and 5 passed at
task 01's end state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-19 12:07:59 -07:00
co-authored by Claude Opus 5
parent 0703306e19
commit ed8cf49540
3 changed files with 136 additions and 5 deletions
+3 -2
View File
@@ -12,7 +12,8 @@ nothing else in the workspace changes.
## Files ## Files
- Copy: `crates/proto/tests/records.rs`, `crates/proto/tests/strict.rs`, - Copy: `crates/proto/tests/records.rs`, `crates/proto/tests/strict.rs` (from
`files/crates/proto/tests/strict.rs-task01`; `files/.../strict.rs` is task 02's),
`crates/proto/tests/fixtures/records/audit.jsonl` `crates/proto/tests/fixtures/records/audit.jsonl`
- Modify: `crates/proto/src/audit.rs`, `crates/proto/src/lib.rs`, `docs/implementer-log.md` - Modify: `crates/proto/src/audit.rs`, `crates/proto/src/lib.rs`, `docs/implementer-log.md`
@@ -92,7 +93,7 @@ In `lib.rs` the audit export becomes
## Steps ## Steps
- [ ] **1. Copy.** `git switch -c m3a`, then - [ ] **1. Copy.** `git switch -c m3a`, then
`cp docs/plans/M3a/files/crates/proto/tests/records.rs docs/plans/M3a/files/crates/proto/tests/strict.rs crates/proto/tests/` `cp docs/plans/M3a/files/crates/proto/tests/records.rs crates/proto/tests/ && cp docs/plans/M3a/files/crates/proto/tests/strict.rs-task01 crates/proto/tests/strict.rs`
and `cp docs/plans/M3a/files/crates/proto/tests/fixtures/records/audit.jsonl crates/proto/tests/fixtures/records/` and `cp docs/plans/M3a/files/crates/proto/tests/fixtures/records/audit.jsonl crates/proto/tests/fixtures/records/`
- [ ] **2. See the tests fail.** `cargo test -p proto --test records`. Expected: it does not - [ ] **2. See the tests fail.** `cargo test -p proto --test records`. Expected: it does not
compile (`AuditEvent` is not found). compile (`AuditEvent` is not found).
+5 -3
View File
@@ -39,7 +39,7 @@ tip T17) and what that check exposed. The detail is in `checks-a.md` to `checks-
| # | File | Delivers | Tests that define it | Check | What the check exposed | | # | File | Delivers | Tests that define it | Check | What the check exposed |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 01 | `01-proto-audit-types.md` | `AuditRecord`, `AuditEvent`, `DecisionRecord` and friends | `proto/tests/records.rs`, `strict.rs`, `fixtures/records/audit.jsonl` | reference (the types) | `DecisionRecord`'s unit variants accepted unknown fields (serde ignores `deny_unknown_fields` there); found by `strict.rs` | | 01 | `01-proto-audit-types.md` | `AuditRecord`, `AuditEvent`, `DecisionRecord` and friends | `proto/tests/records.rs`, `strict.rs`, `fixtures/records/audit.jsonl` | reference (the types) | `DecisionRecord`'s unit variants accepted unknown fields (serde ignores `deny_unknown_fields` there); found by `strict.rs`. After hand-over: the merged `strict.rs` named task 02's fixtures, so task 01's gate could not pass (fixed with `strict.rs-task01`) |
| 02 | `02-proto-admin-wire.md` | the admin messages, `u64` approval ids, `approval_pending`, `tool_denied` | `wire.rs`, `turn_wire.rs`, `admin_wire.rs`, `strict.rs`, 17 wire fixtures | reference (the types) | the same serde hole, found independently; serde's array form for structs (left open in `decisions.md`) | | 02 | `02-proto-admin-wire.md` | the admin messages, `u64` approval ids, `approval_pending`, `tool_denied` | `wire.rs`, `turn_wire.rs`, `admin_wire.rs`, `strict.rs`, 17 wire fixtures | reference (the types) | the same serde hole, found independently; serde's array form for structs (left open in `decisions.md`) |
| 03 | `03-proto-chain-verifier.md` | `ChainVerifier`, `ChainReport` | `proto/tests/chain.rs`, 30 fixture logs with real hashes | reference, generated fixtures, 8 mutants | four spec gaps: which `seq` the lists report, the resumed verifier inside a failed region, a `Recovery` describing nothing, the report fields a writer needs | | 03 | `03-proto-chain-verifier.md` | `ChainVerifier`, `ChainReport` | `proto/tests/chain.rs`, 30 fixture logs with real hashes | reference, generated fixtures, 8 mutants | four spec gaps: which `seq` the lists report, the resumed verifier inside a failed region, a `Recovery` describing nothing, the report fields a writer needs |
| 04 | `04-brokerd-config.md` | `brokerd.toml` into `Config` | `brokerd/tests/config.rs`, 6 fixtures | minimal reference | socket paths when only `home` is set; stale "Used by" column in `dependencies.md` | | 04 | `04-brokerd-config.md` | `brokerd.toml` into `Config` | `brokerd/tests/config.rs`, 6 fixtures | minimal reference | socket paths when only `home` is set; stale "Used by" column in `dependencies.md` |
@@ -64,7 +64,9 @@ tip T17) and what that check exposed. The detail is in `checks-a.md` to `checks-
`files/` holds everything the tasks copy in, byte-identical to the reference tree `files/` holds everything the tasks copy in, byte-identical to the reference tree
(`~/src/boxmaker-ref`, branch `m3a-ref`, unpushed). `files/Makefile-task21` is task 21's (`~/src/boxmaker-ref`, branch `m3a-ref`, unpushed). `files/Makefile-task21` is task 21's
`Makefile`; `files/Makefile` is task 22's, with the end-to-end line. `Makefile`; `files/Makefile` is task 22's, with the end-to-end line. Likewise
`files/crates/proto/tests/strict.rs-task01` is task 01's `strict.rs`; `files/.../strict.rs` is task
02's, which also walks task 02's new wire fixtures.
**State of the checks at hand-over.** In the reference tree `make gate` passes in full, the **State of the checks at hand-over.** In the reference tree `make gate` passes in full, the
end-to-end line included, so every given test has passed against some implementation. Area E's end-to-end line included, so every given test has passed against some implementation. Area E's
@@ -96,7 +98,7 @@ enough. Most spec defects were found by writing test tables and exit lists, not
1. `git log --oneline master..m3a`: twenty-two commits with the trailer. 1. `git log --oneline master..m3a`: twenty-two commits with the trailer.
2. Copied files unchanged: 2. Copied files unchanged:
`for f in $(cd docs/plans/M3a/files && find . -type f ! -name Makefile-task21); do cmp "docs/plans/M3a/files/$f" "$f"; done` `for f in $(cd docs/plans/M3a/files && find . -type f ! -name Makefile-task21 ! -name strict.rs-task01); do cmp "docs/plans/M3a/files/$f" "$f"; done`
3. `git diff master..m3a --stat -- docs/design.md docs/specs docs/plans docs/runbook.md AGENTS.md CLAUDE.md deny.toml` 3. `git diff master..m3a --stat -- docs/design.md docs/specs docs/plans docs/runbook.md AGENTS.md CLAUDE.md deny.toml`
is empty. is empty.
4. `make gate`, `make audit`, `make verify-device`, `tools/check-m3a-device.sh`. 4. `make gate`, `make audit`, `make verify-device`, `tools/check-m3a-device.sh`.
@@ -0,0 +1,128 @@
//! 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",
"turn.json",
"turn_event_tool_result.json",
"turn_event_content.json",
"turn_event_retrying.json",
"turn_done.json",
"error_session_full.json",
] {
// Envelope, msg and body: three objects; turn_done also has a usage object.
let want = if name == "turn_done.json" { 4 } else { 3 };
assert_eq!(
check::<Envelope>(name, &fixture(&format!("wire/{name}"))),
want,
"{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 event: two objects. Decision and approval events also hold an
// outcome object.
let want = if line.contains("\"outcome\"") { 3 } else { 2 };
assert_eq!(
check::<AuditRecord>(&format!("audit.jsonl:{}", i + 1), line),
want
);
}
}
#[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 usage_log_records_reject_unknown_keys_at_every_depth() {
let mut tried = 0;
for (i, line) in fixture("records/session_usage.jsonl").lines().enumerate() {
tried += check::<LogRecord>(&format!("session_usage.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);
}