Repository navigation
Conversation
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · f995307
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Review — no blocking findings
Full review of the whole change (10 files, 2 commits, +2688/-1) via four independent blind fan-out passes grouped by coupling (rocm-core digest-gate + RSA integrity; rocm-core model-selector/readiness; rocm-engine-protocol; lemonade archive-digest + xtask pinned-key + docs), plus driver-level direct reading of the same production source as a cross-check. No narrowing applied — this is this PR's first review.
No ticket named. The PR description's claimed scope (property/exhaustive tests for the download digest gate, the Lemonade archive digest, RSA signature verification, model selection/readiness, the engine protocol, and the pinned release key, plus a docs/testing.md section) matches the diff exactly: the five new test files, two Cargo.toml dev-dependency additions, the one-line Cargo.lock update, and the docs section are the entirety of the change. No production code is touched.
Blocking
None.
Non-blocking
None.
Decisions for the author
- The new proptest files hardcode
cases: Nand never readPROPTEST_CASES— non-blocking-improvement. Three existing modules in this repo (examine_driver_proptests.rs,archive_props.rs, andapps/rocm/src/providers/proptests.rsafter a sibling, not-yet-merged commit) expose a helper that letsPROPTEST_CASESraise a property's case count for a deeper local/CI sweep, while clamping it so it can never lower the count below the safe default (a lesson that sibling commit learned the hard way: an unclamped read letPROPTEST_CASES=0pass every property vacuously). This PR's five new files instead writeProptestConfig { cases: N, ..ProptestConfig::default() }directly, which is not vulnerable to that vacuous-pass bug (the literal field wins over whateverDefaultwould have read from the env), but it also meansPROPTEST_CASEShas zero effect on any of these new suites — an operator cannot opt into a deeper sweep the way they can elsewhere in the repo. Worth adopting the same floor-respecting helper for consistency, not required for correctness. - The exhaustive
listing_a_different_catalog_model_is_never_readinesstest inmodel_selector_properties.rsopens a real loopback TCP socket (bind + spawned thread + HTTP response) for every pair in the full catalog cross-product plus near-miss names — on today's 14-entry catalog this is already on the order of hundreds of real socket round trips in one test, growing quadratically as the catalog does. Nothing here is incorrect (ephemeral ports avoid collisions,server.join()can't hang because the client always connects), but it is a heavier design than necessary for what is fundamentally a pure-function property; an in-process fake ofopenai_models_endpoint_has_model's input, or a lighter transport stub, would keep this test's runtime from scaling with the catalog as new models are added. - The family-word readiness cross-match is pinned, not fixed — tradeoff, already flagged by the author for maintainers.
a_bare_family_word_satisfies_readiness_for_the_whole_familydocuments, in its own doc comment, a known false-positive surface in pre-existing (unchanged by this PR) production matching logic: an engine reporting only a bare family word likeqwenreads as ready for any catalog model of that family, not just the one actually loaded. The test explicitly states this is characterization, not endorsement, and that tightening the behavior is a decision for maintainers. Since the behavior itself is untouched by this PR, this is correctly out of blocking scope — flagging only so the tradeoff is visible to whoever merges.
Positive signals
digest_gate_properties.rs's andintegrity_properties.rs's near-miss generators defensively re-assert that each generated "near miss" candidate actually differs from the true digest/signature before using it (e.g.prop_assert!(!candidate.eq_ignore_ascii_case(&truth), ...)) — a guard against the generator itself silently producing a non-near-miss that would make the property pass vacuously.a_correct_copy_elsewhere_in_the_file_never_rescues_a_wrong_constant-style coverage inxtask/src/verify_pinned_keys.rsplants a correct copy of the canonical key under a different token name before and after the real constant and confirms the check still reports divergence — this genuinely exercises the per-token isolationextract_constantrelies on, rather than only checking "a wrong value exists somewhere in the file."- Every test file in this PR calls the real production function under test (
download_file_streaming,verify_sha256,verify_rsa_pkcs1_sha256_signature,ModelRecipeRecord::matches_ref,openai_models_endpoint_has_model,check_pinned_keys, the realserdeimpls inrocm-engine-protocol) rather than a reimplementation of the logic under test — confirmed in every file by at least one independent pass.
Deployment notes
- This PR's head (
f995307) has a genuine content merge conflict against currentmaininengines/lemonade/src/install.rs: a later, unrelated refactor onmain(after this PR's base commit) movedverify_sha256out ofinstall.rsintoarchive_cache.rs. GitHub's ownmergeable_statefor this PR independently reportsdirty, consistent with this. The two new tests this PR adds toinstall.rswill need to move with that function when this PR is rebased onto currentmain. - Test-only change: no production code, CLI behavior, API, or file-layout change for any consumer.
What this covered
Entire diff (10 files) read completely at PR head f9953072119a32f91edaa5ea86a93985918b6197 against merge-base 4f485bdfb4af367d1274eaa0a9563a1c821ded8c, by four independent blind fan-out workers plus the driver's own direct reading of the production source each test file exercises (net.rs's download digest-gate path, crypto.rs, excerpts of model_catalog.rs/model_readiness.rs, rocm-engine-protocol/src/lib.rs, and the pre-existing production logic in xtask/src/verify_pinned_keys.rs).
- Agent-instruction adherence checked against the root
AGENTS.md(read in full): no violation found; the PR's own checklist items are correctly marked N/A for a test-only change with no message/behavior changes. - History pass: done directly by the driver (
git logon the five pre-existing touched files) — surfaced theinstall.rsdivergence noted under Deployment notes; nothing else of note. - Prior-changes pass: searched this author's own recent commit history for established conventions a new proptest file should follow (surfaced the
PROPTEST_CASES-floor pattern above); no prior review comments existed to check against, since this is the PR's first review. - Code-comments pass: folded into each file-group pass; no contradiction between comments and code beyond one sub-threshold wording imprecision (a doc comment on
decode_all_without_panicdescribing some request-direction types as "output," noted by a worker but not scored as a finding). - CI: every required check on the reviewed commit reports
success(fmt, clippy, the full workspace test matrix, Windows build/test, all engine E2E lanes, the commit-signature/sign-off gate, license headers, lychee, third-party notices). Two early "E2E tests (Strix Halo, Ubuntu, {nightly,release}, current)" runs showcancelled, but later runs of the same check names on the same commit reportsuccess— no outstanding CI gap. - Not independently executed: no Rust toolchain was available in the review environment, so no test in this PR was actually compiled or run by any pass. All conclusions are from tracing the test logic against the real production code (in two cases cross-checked by independently re-deriving the relevant SHA-256/matching-function outputs in Python), not from
cargo test. The PR description's own mutation-testing and runtime claims could not be independently re-executed for the same reason; nothing found contradicts them. - Not reconciled against any discussion on this PR — there was no existing review on this PR at the time this one was written.
🤖 Generated with Claude Code
jussielo-amd
left a comment
There was a problem hiding this comment.
Thanks Romain, this is a thorough suite and every test calls the real production function. One blocker, and it only shows up once the branch meets current main.
Blocking
listing_a_different_catalog_model_is_never_readiness(model_selector_properties.rs:372-426) should fail on current main. #794 grew the catalog from 14 to 49 recipes after this branch's CI ran. Two aliases now trip it:qwen3-32b(onQwen/Qwen3-32B-FP8) andqwen3.6-35b-a3bread as ready forQwen/Qwen3-32B/Qwen/Qwen3.6-35B-A3B, becausemodel_refs_match(net.rs:909-917) returns true whenbasename(right) == left. So the test is catching a real readiness false positive, and the description's "no catalog model reads as ready for a different one" holds only at the old base. Please decide when rebasing: fix readiness, or pin it as a labelled characterization like the bare-family case. (Hand-traced against main's catalog; I could not run it here.)
Rebase note
- One textual conflict, in
engines/lemonade/src/install.rs: #825 movedverify_sha256toengines/lemonade/src/archive_cache.rs. The two new lemonade tests are still valid but need to move into that module (proptest is already a dev-dep there). The description's mention ofinstall.rswill be stale.
Non-blocking
- The digest near-miss generator picks position/keep randomly over 24 cases, so a last-nibble mutant is caught in roughly half the runs; looping all 64 positions makes it deterministic.
- Rejection tests assert only
is_err(), so they'd also pass on a transport failure;serve_once+join()can hang inaccept()if the client never connects. - A few weak/redundant checks:
model_selector_properties.rs:248-277(effectively no-panic),:88(subsumed byvalidate()),protocol_properties.rs:394,:408-427,:548. - The pairwise readiness test is quadratic: ~770 loopback requests now, ~6,200 on main's catalog.
- xtask tests hard-require the current RSA-2048 key shape, so a key rotation breaks them for a non-bug reason.
🤖 by agenthub on AMD AgentHub
Adds property tests over the code that decides whether a downloaded artifact is authentic and the code that decodes untrusted engine output: the download digest gate, the Lemonade archive digest check, RSA signature verification, model selection and readiness, and the engine protocol types. The generators target the near-miss region rather than sampling uniformly: signatures mutated by one byte or re-encoded with leading zeros, digests differing by one nibble or sharing a correct prefix, PEMs with a BOM, stray whitespace or blank lines, loopback spellings padded or re-cased, and model ids with a variant suffix or one character off. A uniform generator never reaches the branches where these checks could fail open. Golden documents pin the wire spellings a round trip cannot see; arbitrary and mutated engine output is smoke-tested for panics. One behaviour the suites pin rather than assert away: readiness matches a bare family word (`qwen`, `llama`) against any model with a name token starting with it, in both directions. An existing unit test exercises one direction (asking for `qwen` is satisfied by a served `Qwen3-0.6B-GGUF`); the pinned consequence is the reverse, where an endpoint reporting only `qwen` reads as ready for other Qwen entries in the catalog too. Selection itself is exact up to case and surrounding whitespace; only the check that confirms the engine is serving the selected model is loose. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The pinned-key check is what guarantees installers trust exactly the key releases are signed with, so a one-character drift during a key rotation must not pass. Runs the real check end to end over a scratch repository root and walks every position of the real published key, in either installer, with a deletion, a duplication, both alphabet neighbours, a case flip and a swap with the next character, rather than sampling random bodies, which any comparison would catch. Also covers a correct copy of the key elsewhere in the same file (before or after the constant), an empty or missing constant against a published key, a blank canonical key (dormant) and a missing installer (error). Pins the reach of the base64 normalization too: whitespace or punctuation inserted into an embedded body is normalized away rather than reported as divergence. Adds a docs/testing.md section listing how to run these suites and the integrity and protocol property tests. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The catalog now carries aliases that spell another catalog model's basename: `qwen3-32b` is an alias of Qwen/Qwen3-32B-FP8 and the basename of Qwen/Qwen3-32B. Readiness accepts a basename for its model, so an engine serving the FP8 recipe under that alias reads as ready for the other one, and the exhaustive "a different catalog model is never readiness" property failed on that. The behaviour is unchanged here; it is excluded from that property and pinned in its own test against the catalog's current list of collisions, so a catalog change that adds one fails rather than being silently skipped. Rejections now have to be the gate's own: the digest tests require the `SHA-256 mismatch` error and that the request reached the server, so a transport failure cannot pass as a rejected digest. The loopback server serves any number of requests and cannot hang when no client connects, which also lets the pairwise readiness test use one server per served name instead of one per request. The one-nibble and prefix near misses are walked over every position instead of sampled. Every property's case count can now be raised by PROPTEST_CASES but not lowered. The Lemonade archive-digest properties took their count from it unchecked, so PROPTEST_CASES=0 passed them without running a case. The removed selector-uniqueness test duplicated the catalog schema's own check, and two protocol properties drop assertions another one in the same test already implied. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The drift tests asserted that the committed key's body was over 300 characters and inserted their noise before the `MII` that starts an RSA key's DER, so rotating to a key of another type or size would fail them for no defect. The coverage check now compares the walked characters with a count taken straight from the committed file, the noise goes at the start of the body, and the decoy test flips a letter on the first body line, which every key has. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
f995307 to
282e946
Compare
|
Thanks. All addressed in b8a6bcc and 282e946, on a branch rebased onto current main. Blocker. Reproduced: after the rebase Rebase. The two Lemonade digest tests moved into Non-blocking.
Also from the automated review: every property's case count can now be raised by |
jussielo-amd
left a comment
There was a problem hiding this comment.
Thanks Romain. My earlier blocker is resolved: I traced the pairwise readiness property against main's 49-recipe catalog and the only alias/basename collisions are exactly qwen3-32b and qwen3.6-35b-a3b, which are now excluded from the property and pinned by an_alias_spelling_another_models_basename_reads_as_ready_for_it. The Lemonade digest tests moved into archive_cache.rs, the LoopbackServer can't hang (accept loop is woken on drop, reads time out), the case-count floor holds at PROPTEST_CASES=0, and the pinned-key tests no longer depend on key shape. CI on this head is green apart from two hardware E2E lanes that are still queued. This was a static review; I did not run the tests.
Non-blocking points are inline (a third copy of the case-count helper, and a hand copy of the download chunk size). One more: the description says a one-bit flip "anywhere" in the Lemonade archive is rejected, but that test flips the last byte plus sampled positions, so "sampled positions" would be more accurate.
🤖 by agenthub on AMD AgentHub
| /// cases it targets, and `PROPTEST_CASES=0` would otherwise pass every | ||
| /// property without running one. A value that is ignored, because it is lower | ||
| /// or does not parse, is reported on stderr. | ||
| pub fn proptest_cases(default: u32) -> u32 { |
There was a problem hiding this comment.
Non-blocking: this is a third copy of proptest_cases/raised_case_count (see crates/rocm-core/src/test_support.rs:192 and apps/rocm/src/test_support.rs:61), and the doc comment on the rocm-core one still says only archive_props keeps its own. Including this file from test_support.rs by #[path], or updating that comment, would keep them from drifting. The #[test] in this file also runs once per includer.
| /// `DOWNLOAD_CHUNK_BYTES` in `download.rs`. The multi-chunk body below spans three | ||
| /// of these plus a byte, so it still crosses a chunk boundary if that constant | ||
| /// grows a little, and regardless of how short the socket reads come back. | ||
| const CHUNK_BYTES: usize = 64 * 1024; |
There was a problem hiding this comment.
Non-blocking: CHUNK_BYTES is a hand copy of the private DOWNLOAD_CHUNK_BYTES in download.rs, with nothing tying them together. The test is built to survive drift (3 chunks plus 1 byte), so this is fine to leave, but exposing the constant to tests would remove the copy.
tests/e2e-cucumber/expectations.tomlfor the fixed ticket ID and removed/narrowed any now-stale xfail rows. — N/A, test-only.docs/architecture.md. — N/A.Summary
The checks that decide whether something
rocmdownloads or runs is authentic — the download digest gate, the Lemonade archive digest, RSA signature verification, the pinned release key — and the code that decodes untrusted engine output had only example tests. Example tests miss exactly the inputs where such a check fails open: a digest one nibble off, a signature re-encoded with a leading zero, a key that differs in one character. This adds property and exhaustive tests aimed at that near-miss region, plus golden documents for the engine wire format.What is covered:
download_file_streaming)crates/rocm-core/tests/digest_gate_properties.rsSHA-256 mismatcherror, and the one-nibble, wrong-suffix and truncated kinds are walked over every position rather than sampled; the true digest is accepted in any ASCII case; a one-bit or past-the-first-chunk body change is rejected and leaves no file behindverify_sha256)engines/lemonade/src/archive_cache.rscrates/rocm-core/tests/integrity_properties.rscrates/rocm-core/tests/model_selector_properties.rscrates/rocm-engine-protocol/tests/protocol_properties.rs./..,--gpuvalues, golden documents for wire spellings a round trip cannot see; arbitrary and mutated input is smoke-tested for panicscargo xtask verify-pinned-keys)xtask/src/verify_pinned_keys.rsShared helpers live in
crates/rocm-core/tests/common/: a loopback HTTP server that answers any number of requests and cannot hang when no client connects, and thePROPTEST_CASESfloor below. Every download and readiness test asserts its request reached that server, so a transport failure cannot pass as a rejection or a "not ready".docs/testing.mdgains a section with the commands.Two behaviours pinned rather than asserted away. Readiness, which confirms the engine is serving the model the user asked for, is looser than selection in two ways. Neither is changed here; each is pinned by a labelled characterization test, so it stays visible. If readiness or the catalog changes, these tests are expected to fail and should be updated with it.
qwenbeing satisfied by a servedQwen3-0.6B-GGUFis already exercised by a unit test inendpoint_readiness.rs. The other direction means an engine whose/v1/modelsreports onlyqwenreads as ready for other Qwen catalog entries too, so readiness cannot tell models of one family apart when that is all the engine reports. Pinned bya_bare_family_word_satisfies_readiness_for_the_whole_familyand its uncatalogued sibling.qwen3-32b(an alias ofQwen/Qwen3-32B-FP8, the basename ofQwen/Qwen3-32B) andqwen3.6-35b-a3b(an alias ofunsloth/Qwen3.6-35B-A3B-GGUF:Q4_K_M, the basename ofQwen/Qwen3.6-35B-A3B). An engine launched under such an alias reads as ready for the other model.an_alias_spelling_another_models_basename_reads_as_ready_for_itderives the collisions from the catalog and compares them with these two, so a catalog change that adds one fails there, rather than being silently skipped by the pairwise property.Whether either is wanted is a question for maintainers: fixing the second means readiness stops accepting a basename that is another recipe's alias, or the catalog drops those aliases.
Can the tests fail?
Each mutation below was applied to the code under test, run against the suite, and reverted.
near_miss_digests_are_rejectedpositional_near_misses_are_rejected_at_every_position(every run),near_miss_digests_are_rejecteda_tampered_body_is_rejected,tampering_past_the_first_chunk_is_rejectedstarts_withinstead of equality; hash only the first 64 KiB; skip an empty pin; last character ignored, including underPROPTEST_CASES=0signature_mutations_are_rejectedlisting_a_different_catalog_model_is_never_readiness,readiness_accepts_a_catalog_model_under_any_of_its_names,a_bare_family_word_satisfies_readiness_for_the_whole_familya_models_document_naming_no_matching_model_never_reports_ready,listing_a_different_catalog_model_is_never_readiness,a_bare_family_word_satisfies_readiness_for_the_whole_familyan_alias_spelling_another_models_basename_reads_as_ready_for_it,a_models_document_listing_the_expected_model_reports_readya_mistyped_selector_selects_nothingdeny_unknown_fieldson the healthcheck;extra_envserialized when empty; success envelope without data; loopback check trimmed, case-folded or widened to0.0.0.0;./..engine-id guards removed; method,DevicePolicy,tokens_per_secorportrenamed;AUTOcase-sensitive; engine-id helper returning nothing; a--gpulist read as its first entry+//dropped from the alphabet; comparison case-insensitive or order-insensitive; extraction reading the whole file or from its start; an empty constant counted as a match; dormant guard or missing-source error removedKnown survivors: removing only one of the two partial-file cleanups in the digest gate (the other still runs, and the test checks the end state). Renames of several protocol fields not listed above are not pinned by a golden document. The pinned-key tests' independence from key shape was not exercised against a key of another type.
Determinism, runtime, platforms
PROPTEST_CASESraises that minimum for a deeper run but never lowers it, soPROPTEST_CASES=0cannot pass a property vacuously. Therocm-coreand protocol suites use proptest's default random seed and persist a regression file next to the test on failure; the Lemonade tests use a fixed seed.integrity_propertiestakes roughly 7–15 s, almost all RSA-2048 key generation (two keys, generated once per test process); the readiness suite takes about 1.5–4 s, since its pairwise test makes about 6,200 requests against the 49-recipe catalog (one server per served name, not per request); every other suite takes under 1 s.AGENTS.md §3
Test-only: no CLI output, exit code, file layout or runtime selection changes, so no Gherkin scenario is needed.
Verification
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy --locked -p e2e-cucumber --test e2e -- -D warnings: clean.cargo test --workspace --all-targets: everything this PR adds passes. Three tests outside it fail on my host only, because of local tooling: twobuild_metadatagit_watch_pathstests, and one container stop test that the host's Docker-compatible shim breaks. They fail the same way onmain.prek run --from-ref origin/main --to-ref HEAD: pass, except two hooks whose tools aren't installed here.hawkeye(license headers) did not run; both new files carry the SPDX header.lycheeskipped itself. CI runs both.Not verified: Windows; runtime under nextest's one-process-per-test model, where each integrity test pays its own key generation;
cargo xtask coverage, which matters because one test that duplicated the catalog schema's own uniqueness check was removed.