From 84af4aa9516dcec0e0e03f4e8b6118fdab0189b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 18:39:42 +0000 Subject: [PATCH 1/3] fix(signals): writes that net in an action's tick pend nothing for a verdict reader (#4485) Writes that net to a signal's committed value, made in the tick an action opens a hold (`setS(2); setS(1)` in the body after `setX(1)`, or on the mainline just before it), made no proposal (D-8): the seam drops the staging and nothing is held. A render effect reading `[s(), isPending(s)]` still showed `[1, true]` for the whole hold and re-ran at the landing as `[1, false]`. Its plain read took `read`'s verdict-reader `stagedScreen` arm before the netted check, listing it as a stale reader of the flush's hold, and its probe was the seam's verdict (`provisionalVerdict`): not final, routed into the verdict lane with a re-run at the landing. `read`'s plain arm now checks netting before the `stagedScreen` arm, and `provisionalVerdict` answers a netted signal staging final: the reader is still the tick's verdict reader, shown now (D-25), with no re-run at the landing. The seat table's plain staged rows gain the same netted check (SLOT_P, nothing raised), which also clears the world mismatches #4452's netted read left in the shadow (10 classes -> 0). #4367's opening tick (`act(3)` then a mainline `set(2)` on s = 2) is the same netted tick, so its interim `pending` frame is re-pinned to the D-8 screen (`F`); its settle after the later writes is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- .changeset/netted-verdict-reader.md | 5 ++ packages/signals/src/core/core.ts | 28 ++++---- packages/signals/src/core/seat.ts | 11 ++- packages/signals/src/core/verdict.ts | 15 +++- ...ction-outside-write-ispending-4367.test.ts | 13 +++- .../tests/netted-verdict-reader-4485.test.ts | 72 +++++++++++++++++++ ...tion-outside-write-ispending-4367.spec.tsx | 9 ++- 7 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 .changeset/netted-verdict-reader.md create mode 100644 packages/signals/tests/netted-verdict-reader-4485.test.ts diff --git a/.changeset/netted-verdict-reader.md b/.changeset/netted-verdict-reader.md new file mode 100644 index 0000000000..16238a5994 --- /dev/null +++ b/.changeset/netted-verdict-reader.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +Writes that net to a signal's committed value in the tick an action opens a hold no longer pend the signal: a render effect reading `isPending(s)` shows `false` during the hold and is not re-run when the action lands (D-8). diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index 868d6fef89..49651fde38 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -2017,19 +2017,23 @@ export function read(el: Signal | Computed): T { if (__TEST__) shadowActual(SLOT_C); return el._value as T; } - // (A plain signal is never uninitialized — the carve-out is `serve`'s.) - // Lane work over a folded lane (D-34), once the frame has a - // transaction, reads the screen too. - if ( - ((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) && - stagedScreen(c as Computed, el) - ) { - if (__TEST__) shadowActual(SLOT_C); - return el._value as T; - } // A staging equal to the committed value is writes that netted (D-8): no - // proposal, so not a read of the frame's future. - if (el._pendingValue !== el._value) stagedRead(c as Computed); + // proposal, so not a read of the frame's future — nor, for a verdict + // reader, a stale read of the tick's hold re-run at its landing (#4485). + if (el._pendingValue !== el._value) { + // (A plain signal is never uninitialized — the carve-out is `serve`'s.) + // Lane work over a folded lane (D-34), once the frame has a + // transaction, reads the screen too. + if ( + ((observeFlightHook && c._config & CONFIG_VERDICT) || + (laneStageHook && passLane?.folded)) && + stagedScreen(c as Computed, el) + ) { + if (__TEST__) shadowActual(SLOT_C); + return el._value as T; + } + stagedRead(c as Computed); + } if (__TEST__) shadowActual(SLOT_P); return el._pendingValue as T; } diff --git a/packages/signals/src/core/seat.ts b/packages/signals/src/core/seat.ts index 876876265d..0f518ff46e 100644 --- a/packages/signals/src/core/seat.ts +++ b/packages/signals/src/core/seat.ts @@ -493,6 +493,11 @@ export function slot( const laneNode = (cfg & CONFIG_OVERRIDE) !== 0; const guess = (cfg & CONFIG_GUESS) !== 0; const stagedError = (cfg & CONFIG_STAGED_ERROR) !== 0; + /** D-8: a plain signal's staging equal to its committed value — writes + * that netted, no proposal (`read`'s plain arm; #4452, #4485). A window's + * read of it is the seam's verdict's rows below (`provisionalVerdict` + * answers it final and routes it as they say). */ + const netted = staged && !isFn && !laneNode && el._pendingValue === el._value; /** The staging as a slot: its value, or the error it stages (D-41). */ const stagedSlot = stagedError ? SLOT_E : SLOT_P; /** The screen as a slot: the committed value, or the error still shown @@ -886,6 +891,11 @@ export function slot( // (F-9, as D1/L1: a staging beneath a flight is not what the reader // sees — the flight decides below. The 50k differential's V3 finding.) if (!pending) { + const row = renderLeaf ? "L1" : inLane ? rowBase + "D7" : rowBase + "D1"; + // D-8: writes that netted made no proposal — the staging (the screen's + // value) for every reader, raising nothing: not a read of the frame's + // future, nor a verdict reader's stale read of the tick's hold. + if (netted) return carry(pred(row, SLOT_P)); if (verdictReader && !uninit && c!._config & CONFIG_VERDICT) { // V2/V3 (D-23 D-32 [R5]): the screen once the tick has a hold, the // staging before (the seam's verdict). @@ -894,7 +904,6 @@ export function slot( } if (inLane && seat!.lane!.folded && stagedScreen && !uninit) return carry(pred(rowBase + "D7'", SLOT_C, MARK_ST, flushTransaction)); // folded lane work reads the screen (D-34) - const row = renderLeaf ? "L1" : inLane ? rowBase + "D7" : rowBase + "D1"; // A pass reading a node born staged (uninitialized) has no screen to // re-derive on: no Fl mark (#3969), the value is still the staging's. const raise = uninit ? 0 : RAISE_FL | (inSeat || (isEffect && verdictReader) ? MARK_W : 0); diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index bfc9b2c8a5..6499c66993 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -178,8 +178,19 @@ function verdictRead(c: Computed, t: Transaction, plain?: boolean): void { * transaction, the reader is its verdict lane's at once; a pass that also * read the frame's own stagings plainly (REACTIVE_STAGED_READ) stays the * frame's (`route` does not move it). */ -function provisionalVerdict(c: Computed, notFinal: boolean): void { +function provisionalVerdict( + c: Computed, + notFinal: boolean, + el?: Signal | Computed +): void { if (flushTransaction) { + // D-8: a signal's staging equal to its committed value is writes that + // netted — no proposal: the seam holds nothing of it and no landing + // flips its verdict, so it is final (F-9, D-23) and the reader is not + // re-run at the landing (#4485; `read`'s plain arm likewise, #4452). The + // reader is still the tick's verdict reader, shown now (D-25: the + // posture is the pass's). A signal's staging only, as `read`'s arm. + if (el && !(el as any)._fn && el._pendingValue === el._value) return route(c, flushTransaction); if (probing && notFinal) probeFound = true; verdictRead(c, flushTransaction); if (probing && c._flags & REACTIVE_STAGED_READ) watchVerdict(c); @@ -540,7 +551,7 @@ function verdictValue(el: Signal | Computed, c: Computed | null): if (owner._statusFlags & STATUS_PENDING && !uninitialized) pendingVerdict(tracked ? c : null, owner); else if (tracked && globalQueue._running) - provisionalVerdict(c!, !latestActive && !uninitialized); + provisionalVerdict(c!, !latestActive && !uninitialized, el); // Outside a flush, the staging is an unflushed write (A28): the probe // answers for the flushed world, and watches the seam of the flush that // carries the write — a memo of `isPending(x)` created after the write diff --git a/packages/signals/tests/action-outside-write-ispending-4367.test.ts b/packages/signals/tests/action-outside-write-ispending-4367.test.ts index f4d32cf328..1c446de4ad 100644 --- a/packages/signals/tests/action-outside-write-ispending-4367.test.ts +++ b/packages/signals/tests/action-outside-write-ispending-4367.test.ts @@ -9,7 +9,9 @@ // `false` at that flush (D-17: a memo between `isPending` and its reader // behaves as the direct read does). A pass outside any transaction's flush // publishes to the screen: its readers re-derive at the seam whatever holds -// the lane's frame. +// the lane's frame. (Re-pinned for #4485: the opening tick's writes net to +// the committed value, so that tick made no proposal and never shows +// `pending` (D-8); the settle after the later writes is unchanged.) import { describe, expect, it } from "vitest"; import { action, @@ -52,7 +54,11 @@ describe("#4367 isPending(s) after an action, with s also written outside it", ( act(3); set(2); await tick(); - expect(frames).toEqual([`F${v(2)}`, `pending${v(2)}`]); + // `set(3)` in the action and `set(2)` outside it are one tick's writes + // to `s`, and they net to its committed value: no proposal, so `s` is + // not held and pends nothing (D-8, #4485) — the screen stays `F`. + // (`s` lands at 2 without the later write.) + expect([...new Set(frames)]).toEqual([`F${v(2)}`]); set(3); flush(); // Nothing differs from the action's write: not pending, in either form. @@ -92,7 +98,8 @@ describe("#4367 isPending(s) after an action, with s also written outside it", ( release(); await tick(); expect(isPending(s)).toBe(false); - expect(frames).toEqual(["F", "pending", "F"]); + // The first tick's writes net (D-8, #4485): never pending. + expect([...new Set(frames)]).toEqual(["F"]); dispose(); }); }); diff --git a/packages/signals/tests/netted-verdict-reader-4485.test.ts b/packages/signals/tests/netted-verdict-reader-4485.test.ts new file mode 100644 index 0000000000..cd40c70741 --- /dev/null +++ b/packages/signals/tests/netted-verdict-reader-4485.test.ts @@ -0,0 +1,72 @@ +// #4485: writes that net to a signal's committed value in the tick an action +// opens a hold made no proposal (D-8), so a verdict reader of the signal +// shows `isPending(s)` false and is not re-run at the landing. It showed +// `true` for the whole hold and re-ran at the landing: its plain read took +// the `stagedScreen` arm before any netted check, which listed it as a stale +// reader of the flush's hold, and its probe was the seam's verdict +// (`provisionalVerdict`), not final (D-23 Status, #4469's family). +import { describe, expect, it } from "vitest"; +import { + action, + createRenderEffect, + createRoot, + createSignal, + flush, + isPending +} from "../src/index.js"; + +const tick = async () => { + await new Promise(r => setTimeout(r, 0)); + flush(); +}; + +describe("#4485: a verdict reader of a signal whose writes net", () => { + const net = async (where: "in-action" | "before-action" | "later-tick") => { + const [s, setS] = createSignal(1); + const [x, setX] = createSignal(0); + const runs: unknown[] = []; + const dispose = createRoot(d => { + createRenderEffect(x, () => {}); + createRenderEffect( + () => [s(), isPending(s)], + v => void runs.push(v) + ); + return d; + }); + flush(); + runs.length = 0; + if (where === "before-action") (setS(2), setS(1)); + let release!: () => void; + action(function* () { + setX(1); + if (where === "in-action") (setS(2), setS(1)); + yield new Promise(r => (release = r)); + })(); + if (where === "later-tick") { + await tick(); + setS(2); + setS(1); + } + await tick(); + const held = runs.slice(); + release(); + await tick(); + await tick(); + dispose(); + return { held, runs }; + }; + + it.each(["in-action", "before-action"] as const)( + "pends nothing and is not re-run at the landing (%s)", + async where => { + const { held, runs } = await net(where); + expect(held).toEqual([[1, false]]); + expect(runs).toEqual([[1, false]]); + } + ); + + it("pends nothing in a later tick of the hold", async () => { + const { runs } = await net("later-tick"); + expect(runs).toEqual([[1, false]]); + }); +}); diff --git a/packages/web/test/action-outside-write-ispending-4367.spec.tsx b/packages/web/test/action-outside-write-ispending-4367.spec.tsx index 272420b57b..ade69ba15c 100644 --- a/packages/web/test/action-outside-write-ispending-4367.spec.tsx +++ b/packages/web/test/action-outside-write-ispending-4367.spec.tsx @@ -7,7 +7,9 @@ // action's value ends the pending verdict; ``'s condition memo leaves // the verdict lane equal to its committed value. The screen must follow, as // it does for the inline `{isPending(s) ? … : …}` — and once the action -// completes, the fallback shows for good. +// completes, the fallback shows for good. (Re-pinned for #4485: the opening +// tick's writes net to the committed value, so it never shows `pending`, +// D-8.) import { expect, test } from "vitest"; import { Show, action, createSignal, flush, isPending } from "solid-js"; import { render } from "../src/index.js"; @@ -42,7 +44,10 @@ for (const shape of ["Show", "inline"] as const) set(2); await tick(); flush(); - expect(el.innerHTML).toBe("pending"); + // `set(3)` in the action and `set(2)` outside it are one tick's writes to + // `s`, and they net to its committed value: no proposal, so `s` is not + // held and pends nothing (D-8, #4485). + expect(el.innerHTML).toBe("[F]"); set(3); flush(); // Nothing differs from the action's write any more: not pending. From 9a0dee9b00cfda50494c77998939405ce4b074d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 18:39:43 +0000 Subject: [PATCH 2/3] chore(size): raise the isPending/latest cap for the netted verdict read +69 B minified over next on every bundle that imports the verdict engine (`provisionalVerdict`'s netted arm, and `read`'s plain arm no longer folding into one ternary). Only "signals: + isPending/latest" crosses its cap: 11.12 -> 11.14 KB at measured 11,130 B + 10 B, recorded minified 30,641 -> 30,725 B. The live server-components frozen floor stays within its cap (38,816 B against 38.85 KB) and is not raised. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- scripts/size/scenarios.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index f258ebe203..29810707b6 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1443,9 +1443,10 @@ module.exports = [ // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 11.07 KB -> 11.10 KB, recorded minified 30,492 -> 30,569 B (+77 B), measured locally (linux) at 11,085 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 30,569 -> 30,596 B (+27 B), measured locally (linux) at 11,092 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 11.10 KB -> 11.12 KB, recorded minified 30,596 -> 30,641 B (+45 B), measured locally (linux) at 11,109 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.12 KB", + // Writes that net to a signal's committed value in the tick an action opens a hold pend nothing (D-8, #4485, 2026-10-30, over next @ ab3d2add): 11.12 KB -> 11.14 KB, recorded minified 30,641 -> 30,725 B (+84 B; +69 B over next), measured locally (linux) at 11,130 B brotli. `provisionalVerdict` answers a netted signal staging final and routes its reader without a re-run at the landing; `read`'s plain arm skips `stagedScreen` for it. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "11.14 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30641, + capMinified: 30725, alias }, { From 31d7847435e7b73bf6d9bcbe098f81ac12ef4a6a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 20:48:42 +0000 Subject: [PATCH 3/3] perf(signals): fold #4485's netted arm into read's tail and verdictValue's call site Restores read's single return-the-screen tail (the netted staging returns the committed value through it, shadow slot P) and moves the D-8 netted route out of provisionalVerdict into its one caller in verdictValue, so provisionalVerdict keeps its old signature. No behaviour change: signals 6103 passed / 15 expected fail; web 1581 passed / 7 expected fail. Pins #4367's re-pinned frames exactly (run counts included) instead of de-duplicated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- packages/signals/src/core/core.ts | 26 +++++++++---------- packages/signals/src/core/verdict.ts | 23 +++++++--------- ...ction-outside-write-ispending-4367.test.ts | 7 ++--- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index f917802c3f..f5f4707582 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -2065,21 +2065,19 @@ export function read(el: Signal | Computed): T { } // A staging equal to the committed value is writes that netted (D-8): no // proposal, so not a read of the frame's future — nor, for a verdict - // reader, a stale read of the tick's hold re-run at its landing (#4485). - if (el._pendingValue !== el._value) { - // (A plain signal is never uninitialized — the carve-out is `serve`'s.) - // Lane work over a folded lane (D-34), once the frame has a - // transaction, reads the screen too. - if ( - ((observeFlightHook && c._config & CONFIG_VERDICT) || - (laneStageHook && passLane?.folded)) && - stagedScreen(c as Computed, el) - ) { - if (__TEST__) shadowActual(SLOT_C); - return el._value as T; - } - stagedRead(c as Computed); + // reader, a stale read of the tick's hold re-run at its landing (#4485): + // the committed value is the staging. (A plain signal is never + // uninitialized — the carve-out is `serve`'s.) Lane work over a folded + // lane (D-34), once the frame has a transaction, reads the screen too. + if ( + el._pendingValue === el._value || + (((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) && + stagedScreen(c as Computed, el)) + ) { + if (__TEST__) shadowActual(el._pendingValue === el._value ? SLOT_P : SLOT_C); + return el._value as T; } + stagedRead(c as Computed); if (__TEST__) shadowActual(SLOT_P); return el._pendingValue as T; } diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index 344fe2b925..c3ca6fb1c5 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -191,19 +191,8 @@ function verdictRead(c: Computed, t: Transaction, plain?: boolean): void { * transaction, the reader is its verdict lane's at once; a pass that also * read the frame's own stagings plainly (REACTIVE_STAGED_READ) stays the * frame's (`route` does not move it). */ -function provisionalVerdict( - c: Computed, - notFinal: boolean, - el?: Signal | Computed -): void { +function provisionalVerdict(c: Computed, notFinal: boolean): void { if (flushTransaction) { - // D-8: a signal's staging equal to its committed value is writes that - // netted — no proposal: the seam holds nothing of it and no landing - // flips its verdict, so it is final (F-9, D-23) and the reader is not - // re-run at the landing (#4485; `read`'s plain arm likewise, #4452). The - // reader is still the tick's verdict reader, shown now (D-25: the - // posture is the pass's). A signal's staging only, as `read`'s arm. - if (el && !(el as any)._fn && el._pendingValue === el._value) return route(c, flushTransaction); if (probing && notFinal) probeFound = true; verdictRead(c, flushTransaction); if (probing && c._flags & REACTIVE_STAGED_READ) watchVerdict(c); @@ -574,8 +563,16 @@ function verdictValue(el: Signal | Computed, c: Computed | null): (!uninitialized || (probing && !(el._config & CONFIG_STAGED_ERROR))) ) pendingVerdict(tracked ? c : null, owner); + // D-8: a signal's staging equal to its committed value is writes that + // netted — no proposal: the seam holds nothing of it and no landing + // flips its verdict, so it is final (F-9, D-23) and the reader is not + // re-run at the landing (#4485; `read`'s plain arm likewise, #4452). The + // reader is still the tick's verdict reader, shown now (D-25: the + // posture is the pass's). A signal's staging only, as `read`'s arm. else if (tracked && globalQueue._running) - provisionalVerdict(c!, !latestActive && !uninitialized, el); + flushTransaction && !(el as any)._fn && el._pendingValue === el._value + ? route(c!, flushTransaction) + : provisionalVerdict(c!, !latestActive && !uninitialized); // Outside a flush, the staging is an unflushed write (A28): the probe // answers for the flushed world, and watches the seam of the flush that // carries the write — a memo of `isPending(x)` created after the write diff --git a/packages/signals/tests/action-outside-write-ispending-4367.test.ts b/packages/signals/tests/action-outside-write-ispending-4367.test.ts index 1c446de4ad..e20988c245 100644 --- a/packages/signals/tests/action-outside-write-ispending-4367.test.ts +++ b/packages/signals/tests/action-outside-write-ispending-4367.test.ts @@ -57,8 +57,9 @@ describe("#4367 isPending(s) after an action, with s also written outside it", ( // `set(3)` in the action and `set(2)` outside it are one tick's writes // to `s`, and they net to its committed value: no proposal, so `s` is // not held and pends nothing (D-8, #4485) — the screen stays `F`. - // (`s` lands at 2 without the later write.) - expect([...new Set(frames)]).toEqual([`F${v(2)}`]); + // (`s` lands at 2 without the later write.) The memo form's reader + // re-runs only when it reads `s()` beside the verdict. + expect(frames).toEqual(memo && !value ? ["F"] : [`F${v(2)}`, `F${v(2)}`]); set(3); flush(); // Nothing differs from the action's write: not pending, in either form. @@ -99,7 +100,7 @@ describe("#4367 isPending(s) after an action, with s also written outside it", ( await tick(); expect(isPending(s)).toBe(false); // The first tick's writes net (D-8, #4485): never pending. - expect([...new Set(frames)]).toEqual(["F"]); + expect(frames).toEqual(["F", "F"]); dispose(); }); });