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:
@@ -12,7 +12,8 @@ nothing else in the workspace changes.
|
||||
|
||||
## 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`
|
||||
- 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
|
||||
|
||||
- [ ] **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/`
|
||||
- [ ] **2. See the tests fail.** `cargo test -p proto --test records`. Expected: it does not
|
||||
compile (`AuditEvent` is not found).
|
||||
|
||||
@@ -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 |
|
||||
|---|---|---|---|---|---|
|
||||
| 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`) |
|
||||
| 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` |
|
||||
@@ -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
|
||||
(`~/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
|
||||
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.
|
||||
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`
|
||||
is empty.
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user