Skip to content

fix(core): reset-frame! preserves image generation + adversarial reset tests (rf2-qnk02m, rf2-060di5)#4889

Merged
mike-thompson-day8 merged 1 commit into
mainfrom
worker/cr-reset-frame-qnk02m
Jun 22, 2026
Merged

fix(core): reset-frame! preserves image generation + adversarial reset tests (rf2-qnk02m, rf2-060di5)#4889
mike-thompson-day8 merged 1 commit into
mainfrom
worker/cr-reset-frame-qnk02m

Conversation

@mike-thompson-day8

Copy link
Copy Markdown
Contributor

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 by make-frame before reg-frame ever saw it) AND :rf.frame/generation (stripped into the :generation slot by new-frame-record) absent. So the recreated frame got :generation nil and 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-events replay dispatched events whose handlers no longer resolved — and dispatch-sync traces-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 :generation before destroy-frame! and re-threads it through the reserved :rf.frame/generation construction key, so new-frame-record seats it onto the recreated record's :generation slot before the :initial-events replay 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-events against the SAME resolved frame definition. Re-resolving from :images (the bead's option b) is impossible here: :images are consumed by make-frame and 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)

  1. 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.
  2. 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-events replay 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 to nil, :written-by was :global (registrar degrade), :n was nil — the exact qnk02m symptom. Restored the fix; all green.

Quality gates

  • npm run test:cljs7955 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.

…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
@mike-thompson-day8 mike-thompson-day8 merged commit a9d9424 into main Jun 22, 2026
57 checks passed
@mike-thompson-day8 mike-thompson-day8 deleted the worker/cr-reset-frame-qnk02m branch June 22, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant