fix(core): reset-frame! preserves image generation + adversarial reset tests (rf2-qnk02m, rf2-060di5)#4889
Merged
Conversation
…t tests reset-frame! re-registered an image-loaded frame from its stored :config, but :config has :images consumed-and-absent and :rf.frame/generation stripped, so the recreated frame got :generation nil and silently degraded to registrar resolution. If the image carried inline-only registrations, the :initial-events replay hit unregistered handlers that dispatch-sync traces-and-recovers (no throw) — leaving a live frame in a wrong/empty state with no loud signal. Fix: snapshot the live :generation before destroy and re-thread it through the reserved :rf.frame/generation construction key, so new-frame-record seats it onto the recreated record before the :initial-events replay runs. The replay then resolves through the frame's OWN image generation exactly as the original construction did (the EP-0027 reset contract: replay against the SAME resolved frame definition). Ordinary (no-image) frames carry :generation nil; the re-thread is a no-op there, byte-identical to before. Tests (frame_initial_events_cljs_test): (1) repeated-reset idempotency — reset twice equals one reset, reset -> mutate -> reset returns to the recorded seed; (2) the image + inline-only-registration + reset case that reproduces the bug — asserts the recreated frame keeps its generation and the inline handlers resolve on replay. Both new assertion groups fail without the fix (generation lost, replay resolves :global / leaves db nil). rf2-qnk02m, rf2-060di5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
reset-frame!(EP-0027 §Reset) lost an image-loaded frame's resolved image generation. It re-registered the frame from its stored:config, but that config has BOTH:images(consumed bymake-framebeforereg-frameever saw it) AND:rf.frame/generation(stripped into the:generationslot bynew-frame-record) absent. So the recreated frame got:generation niland silently degraded to registrar resolution.The dangerous case: if the image carried inline-only registrations (present only in the generation, never the global registrar), the
:initial-eventsreplay dispatched events whose handlers no longer resolved — anddispatch-synctraces-and-recovers an unregistered handler (no throw). The result was a live frame in a wrong/empty state with no loud signal.Fix chosen: re-thread the resolved generation (not fail-loud)
reset-frame!now snapshots the live:generationbeforedestroy-frame!and re-threads it through the reserved:rf.frame/generationconstruction key, sonew-frame-recordseats it onto the recreated record's:generationslot before the:initial-eventsreplay runs. The replay then resolves through the frame's OWN image generation exactly as the original construction did.This matches the EP-0027 reset contract — reset replays
:initial-eventsagainst the SAME resolved frame definition. Re-resolving from:images(the bead's option b) is impossible here::imagesare consumed bymake-frameand never stored on the frame record, so the already-resolved generation is the only thing available to preserve. Fail-loud (option c) was rejected because reset on an image-loaded frame is a legitimate, well-defined operation once the generation is preserved — there is no reason to refuse it. An ordinary (no-image) frame carries:generation nil; the re-thread is a no-op there (registrar resolution, byte-identical to before).Tests added (frame_initial_events_cljs_test)
reset-frame-repeated-reset-is-idempotent(rf2-060di5) — reset twice in a row equals one reset; reset → mutate → reset returns to the recorded seed. Guards the double-apply / clear-recording regression class the single-reset test could not catch.reset-frame-image-loaded-keeps-generation-inline-resolves(rf2-qnk02m) — the test that CATCHES the bug. An image-loaded frame whose image carries inline-only registrations, reset, then asserts (a) the recreated frame still carries a resolved generation and (b) the:initial-eventsreplay resolves the INLINE handlers (writes:inline), not the global registrar.Confirmed the bug-repro test fails without the fix (temporarily reverted
reset-frame!to the buggy body): generation resolved tonil,:written-bywas:global(registrar degrade),:nwasnil— the exact qnk02m symptom. Restored the fix; all green.Quality gates
npm run test:cljs— 7955 tests, 36607 assertions, 0 failures, 0 errors (new + existing green).clojure -M:test(JVM core) — 1675 tests, 7290 assertions, 0 failures, 0 errors.Touched only
implementation/core(frame.cljc+ the core reset/construction test ns). No classification code, no spec/conformance fixtures.