test: pin that self_sk survives a delegate re-key migration (#612) - #613
Conversation
|
Two notes for reviewers:
This PR is on hold and must not be merged — see the pinned hold note below. [AI-assisted - Claude] |
Independent review (Light tier) — completeTwo independent lenses, both blind to each other, neither wrote the code:
Neither found a blocking issue. The code-first lens independently re-traced every factual claim in the new doc comments against the actual code — the Findings and dispositions1. (should-fix) The delegate-side Fixed in Source-scraped rather than behavioral, deliberately: on native targets Mutation-checked in three directions rather than assumed:
The first attempt at the second mutation inserted a bare 2. (should-fix, judgment call) No test covers "an upstream construction site quietly stops populating Not fixed here, deliberately. Three reasons: (a) it is a different hazard from #612 — that issue is about the storage location leaving the indexed path, whereas this is a plain population bug in room creation, with a different fix and a different owner; (b) the shape is already partially pinned by the pre-existing 3. (nit, both lenses) The negative test's hand-rolled Acknowledged, not changed. That is the intended tradeoff for a tripwire of this kind, and it is why the warning was placed at the field definition rather than only in the issue tracker. The positive test is the load-bearing one and does not depend on the guess. Verified non-vacuous today: ciborium encodes structs as name-keyed maps, so field order cannot drift the test into passing for the wrong reason, and every 4. (nit) Now 894 tests pass locally; [AI-assisted - Claude] |
MergedFable 5 review returned MERGE AS-IS (no blocking findings), all checks passed on The reviewer re-ran the mutation checks independently rather than trusting the PR body, reproducing the reader-mutation result exactly (893 pass, exactly 1 fail — Two non-blocking nits are recorded on #612 rather than left in this thread. [AI-assisted - Claude] |
… the real migration gate Three review fixes: 1. The guard's header cited five prior incidents as the class it fixes, but two of them (river-core's bulk lib tests, the nine common/tests allowlist files) happened while river-core DID have -p test steps — a configuration the guard passes. It checks presence, not adequacy. Says so now, since a guard overclaiming its coverage is the failure mode this PR is about. 2. `\s` is GNU-only, so the script failed spuriously on macOS/BSD. Uses [[:space:]] throughout. 3. The build.yml caveat said what the delegate tests don't cover but not where that coverage lives. Now points at the #613 pin in `cargo test -p river-ui --bins`, and cites handlers.rs:191-198 (191 is the cfg arm; 190 was the prose comment). Also parses `members = [...]` by accumulating to the closing bracket, so a single-line reformat is handled instead of running to EOF and reporting "member '2' has no Cargo.toml" (it had picked up `resolver = "2"`). [AI-assisted - Claude]
#615) * ci: run chat-delegate and web-container tests, guard the gap class The chat-delegate's 40 unit tests never ran in CI. Makefile.toml defines test-chat-delegate and rolls it into `cargo make test`, but no workflow invoked it, so the tests gated only a developer's local run. Same for web-container-contract + web-container-tool (9 tests). Adds a cargo test step for each, plus scripts/check-ci-test-coverage.sh, which fails CI when any [workspace] member has no `cargo test -p <name>` step in build.yml. That converts a recurring per-crate oversight into a CI failure. Closes #614 Refs #612, freenet/freenet-core#2776 [AI-assisted - Claude] * ci: address review — scope the guard's claim, POSIX regexes, point to the real migration gate Three review fixes: 1. The guard's header cited five prior incidents as the class it fixes, but two of them (river-core's bulk lib tests, the nine common/tests allowlist files) happened while river-core DID have -p test steps — a configuration the guard passes. It checks presence, not adequacy. Says so now, since a guard overclaiming its coverage is the failure mode this PR is about. 2. `\s` is GNU-only, so the script failed spuriously on macOS/BSD. Uses [[:space:]] throughout. 3. The build.yml caveat said what the delegate tests don't cover but not where that coverage lives. Now points at the #613 pin in `cargo test -p river-ui --bins`, and cites handlers.rs:191-198 (191 is the cfg arm; 190 was the prose comment). Also parses `members = [...]` by accumulating to the closing bracket, so a single-line reformat is handled instead of running to EOF and reporting "member '2' has no Cargo.toml" (it had picked up `resolver = "2"`). [AI-assisted - Claude]
Problem
RoomData::self_skis a room's actual signing/identity private key. It is notderived from anything and cannot be regenerated — if it is lost, the room is
lost.
Nothing deliberately protects it across a delegate WASM re-key. It survives only
because it happens to sit inside the
RoomDatablob stored under the generic,indexed delegate key
room:<b58 owner_vk>, and the delegate'skey_indexisexactly what the migration probe's
ListRequestenumerates. Move the key intoits own bespoke secret the way
handle_store_signing_keydoes (that writer nevertouches
key_index) and it silently leaves the indexed set and stops migrating.That refactor is a reasonable-looking hardening move — a raw private key does not
belong in a multi-purpose blob — which is what makes it dangerous: it reads as
pure improvement. The
signing_key:secret gets away with the pattern onlybecause it is regenerable (
migrate_signing_keyre-derives it fromself_skafter every migration).
self_skhas no such fallback.River re-keys its delegate roughly weekly, so this lands as "every user's
identity in every room is gone" at the next routine release, with no error raised
on any path.
#612 documents the hazard. An issue is a sign, not a fence — this PR
adds the fence.
Approach
Test-only, plus documentation. No production behavior change.
Two paired tests in
freenet_api::response_handler::tests, driving the realcode on both sides of the re-key rather than asserting the field exists:
self_sk_survives_a_legacy_delegate_migration— the predecessor's blob comesfrom
reconcile_room_present(the sole production writer ofroom:<vk>);discovery runs through the real
plan_load_from_keys; the successor's read isthe same
from_reader::<RoomSlot>+reconstruct_roomspair thatmigrate_legacy_per_roomruns. Then it asserts the key arrived intact.a_relocated_self_sk_would_not_survive_a_legacy_delegate_migration— proves thenegative. Stores a blob shaped the way the relocation would shape it, holds
everything else identical, runs the same migration reader, and shows the key
does not come across while the room migrates fine and no error is raised
anywhere. Without this, the positive test could pass for the wrong reason.
A third test added in review,
only_the_indexed_delegate_paths_register_keys_for_migration,pins the delegate-side mechanism the other two depend on: the generic
handle_store_request/handle_cas_store_requestpaths must register their keysin
key_index,handle_store_signing_keymust not, andhandle_list_requestmustanswer from that index. Source-scraped because the behavioral form cannot fail on
native targets (
DelegateCtx::set_secretis a no-op,get_secretalways returnsNone), so it would pass whether or not the handler indexed anything.Documentation at the two points of temptation, both pointing at #612 and the
tests: the
self_skfield definition (ui/src/room_data.rs) andreconcile_room_present(ui/src/components/app/chat_delegate.rs).The only non-test change is widening
reconcile_room_presentfromfntopub(crate) fnso the test can use the real writer instead of hand-rolling theblob. No logic change; nothing outside its module calls it.
The tests live in
river-ui, which CI gates viacargo test -p river-ui --bins.Testing
cargo test -p river-ui --bins— 894 pass. Also green under--features example-data,no-sync(898).cargo fmtandcargo clippyclean(remaining warnings are pre-existing on main).
Both tests were mutation-checked, not just observed passing:
merged.self_sk = Nonebefore serialization inreconcile_room_present(simulating the relocation dropping it from theblob). The positive test fails with its intended message. Two pre-existing
writer tests (
a_saved_slot_always_carries_self_vk,reconcile_room_present_diverged_identity_keeps_local) also catch this one —reported for accuracy; this PR is not the sole guard against that particular
edit.
self_skwhenreconstruct_roomsinserts aPresentslot, which is Do not move self_sk out of RoomData's generic migration path without a compensating migration mechanism #612's exact failure mode (the migration path stopscarrying the key). Only the new test catches it: 892 other tests pass.
That is the gap this PR closes.
The negative test passes under both mutations, as it should — it never touches
the mutated code, which is what keeps the pair independent.
The third test was mutation-checked in three further directions (indexed path stops
indexing / bespoke path starts indexing / list anchor renamed) — all caught. Details,
including one mutation that initially did not fire and why, are in the review comment
below.
All mutations were reverted; the branch is the clean state.
Reviewed by two independent lenses (code-first, testing); no blocking findings. The one
should-fix is the third test above; dispositions for the rest are in the review comment.
Refs #612. Surfaced #614 (chat-delegate tests never run in CI).
[AI-assisted - Claude]