Skip to content

Commit 49902fd

Browse files
Duncanwpfleger96
andcommitted
test(sidebar-sync): add hook/UI-state oracle to Carl adoption tests
Three assertion-only additions to close Thufir's pass-4 BLOCK: 1. runWholeBlobCarlSuite + runWholeBlobP2a1HookSuite each gain an assertHookState parameter (lane-specific hook oracle). Both callers (sections: .sections/.assignments; sort: .sortModeFor("remote")) pass assertions that target hook.result.current after the act() flush. 2. The four named test sites call assertHookState after the storage and outbox assertions, independently verifying React state. The erroneous "collapsed into storage" comment is removed. 3. Removed the applyRemote-returns-prev class of regression: if the callback writes storage but returns stale React state the hook tests now go red while the storage assertions stay green — exactly the mutation Thufir proved in his disposable tree. 4. wholeBlobSyncCarl.shared.test.mjs trimmed from 1009 to 995 lines by collapsing redundant inline comments (already covered in JSDoc). All touched files ≤1000 lines (wc -l). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
1 parent 43e22e1 commit 49902fd

3 files changed

Lines changed: 65 additions & 41 deletions

File tree

desktop/src/features/sidebar/lib/useChannelSections.test.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ runWholeBlobCarlSuite({
257257
sections: [{ id: "remote", name: "Remote", order: 0 }],
258258
assignments: {},
259259
}),
260+
assertHookState: (r, label) => {
261+
assert.deepEqual(
262+
r.sections,
263+
[{ id: "remote", name: "Remote", order: 0 }],
264+
`P*/hook ${label}: hook.sections must reflect adopted remote store — applyRemote returning prev leaves UI stale`,
265+
);
266+
assert.deepEqual(
267+
r.assignments,
268+
{},
269+
`P*/hook ${label}: hook.assignments must reflect adopted remote store`,
270+
);
271+
},
260272
});
261273

262274
runWholeBlobP2a1Suite({
@@ -359,4 +371,16 @@ runWholeBlobP2a1HookSuite({
359371
sections: [{ id: "remote", name: "Remote", order: 0 }],
360372
assignments: {},
361373
}),
374+
assertHookState: (r, label) => {
375+
assert.deepEqual(
376+
r.sections,
377+
[{ id: "remote", name: "Remote", order: 0 }],
378+
`P2a-1/hook ${label}: hook.sections must reflect adopted H102 store — applyRemote returning prev leaves UI stale`,
379+
);
380+
assert.deepEqual(
381+
r.assignments,
382+
{},
383+
`P2a-1/hook ${label}: hook.assignments must reflect adopted H102 store`,
384+
);
385+
},
362386
});

desktop/src/features/sidebar/lib/useChannelSortPreference.test.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ runWholeBlobCarlSuite({
164164
useHook: useChannelSortPreference,
165165
makeEditStore: () => ({ version: 1, groups: { click: "alpha" } }),
166166
makeRemoteStore: () => ({ version: 1, groups: { remote: "recent" } }),
167+
assertHookState: (r, label) => {
168+
assert.equal(
169+
r.sortModeFor("remote"),
170+
"recent",
171+
`P*/hook ${label}: hook.sortModeFor("remote") must reflect adopted remote store — applyRemote returning prev leaves UI stale`,
172+
);
173+
},
167174
});
168175

169176
runWholeBlobP2a1Suite({
@@ -218,4 +225,11 @@ runWholeBlobP2a1HookSuite({
218225
makeEdit: (r) => r.setSortModeFor("channels", "alpha"),
219226
makeEditStore: () => ({ version: 1, groups: { click: "alpha" } }),
220227
makeRemoteStore: () => ({ version: 1, groups: { remote: "recent" } }),
228+
assertHookState: (r, label) => {
229+
assert.equal(
230+
r.sortModeFor("remote"),
231+
"recent",
232+
`P2a-1/hook ${label}: hook.sortModeFor("remote") must reflect adopted H102 store — applyRemote returning prev leaves UI stale`,
233+
);
234+
},
221235
});

desktop/src/features/sidebar/lib/wholeBlobSyncCarl.shared.test.mjs

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const { stubRelay } = makeHookStubs();
6464
* @param {Function} opts.useHook the hook under test
6565
* @param {Function} opts.makeEditStore () => store (the restored edit)
6666
* @param {Function} opts.makeRemoteStore () => store (peer head content)
67+
* @param {Function} opts.assertHookState (hookResult, label) => void (lane-specific hook oracle)
6768
*/
6869
export function runWholeBlobCarlSuite({
6970
label,
@@ -74,6 +75,7 @@ export function runWholeBlobCarlSuite({
7475
useHook,
7576
makeEditStore,
7677
makeRemoteStore,
78+
assertHookState,
7779
}) {
7880
// ── P1/C1-publish: failed bootstrap → restored outbox (queuedAt=200) →
7981
// relay head at createdAt=100 → MUST PUBLISH (restored edit is newer).
@@ -212,19 +214,13 @@ export function runWholeBlobCarlSuite({
212214
// ── P1/C1-stale: failed bootstrap → restored outbox (queuedAt=100) →
213215
// relay head at createdAt=200 → MUST ADOPT (head is strictly newer).
214216
//
215-
// Production sequence:
216-
// 1. Outbox seeded with queuedAt=100.
217-
// 2. Bootstrap fails → result.action="hold" → shouldReplay=true.
218-
// 3. hook .then(): publishSections(store, true, 100) → pendingRestoredQueuedAt=100.
219-
// 4. Debounce. fetchOwnBlobBeforePublish returns peerHead (createdAt=200).
220-
// !pendingIsRestoredReplay guard suppresses the failed-bootstrap exception.
221-
// Restored adopt-guard: remote.createdAt(200) > queuedAt(100) → ADOPT.
222-
// Outbox is cleared (head supersedes old edit). Zero publishes.
217+
// Bootstrap fails → shouldReplay=true → pendingRestoredQueuedAt=100.
218+
// Debounce: peerHead createdAt=200. !pendingIsRestoredReplay guard suppresses
219+
// the failed-bootstrap exception; restored adopt-guard: 200>100 → ADOPT.
220+
// Outbox cleared; zero publishes. Hook and storage reflect remote store.
223221
//
224-
// Causality mutation: remove !pendingIsRestoredReplay guard
225-
// → failed-bootstrap exception fires → publishBaseline absorbs 200
226-
// → returns {kind:"publish"} → publishes old stale edit OVER newer head
227-
// → test FAILS (publishCalls.length > 0, no adopt, outbox not cleared).
222+
// Mutation (!pendingIsRestoredReplay removed): exception fires → publishBaseline
223+
// absorbs 200 → publishes stale edit over newer head → test FAILS.
228224
test(`P1/C1-stale ${label}: failed-bootstrap hook replay — stale restored edit (queuedAt=100) must ADOPT newer relay head (createdAt=200), not publish over it`, async () => {
229225
const { act, cleanup, renderHook } = await import("@testing-library/react");
230226
const pubkey = `pk-c1-stale-${label}`;
@@ -310,10 +306,9 @@ export function runWholeBlobCarlSuite({
310306
`remove onRemoteAdopted callback → old edit store remains persisted`,
311307
);
312308

313-
// (b) Hook state reflects the adopted remote store (driven by localStorage).
314-
// After act+microtasks above the hook has already re-rendered from the adopted value.
315-
// We use the persisted value as the oracle because it is the source of hook state.
316-
// (assertion collapsed into (a) above — same invariant, same mutation tripwire)
309+
// (b) Hook/UI state must reflect the adopted remote store independently of storage.
310+
// Mutation (applyRemote returns prev): storage correct, React state stale → fails.
311+
assertHookState(hook.result.current, label);
317312

318313
// (c) Losing own outbox must be cleared — head supersedes the stale edit.
319314
// Mutation (clearOutbox removed from clearPendingState): old outbox persists → not null → fails.
@@ -610,26 +605,18 @@ export function runWholeBlobP2a1Suite({
610605
/**
611606
* Hook-layer P2a-1 regression (IMPORTANT 3).
612607
*
613-
* Drives the blocked-bootstrap sequence through the ACTUAL React hook so that
608+
* Drives the blocked-bootstrap sequence through the actual React hook so that
614609
* shouldReplay, outbox read, queuedAt threading, and the real .then() callback
615-
* are all exercised. The manager-layer test (runWholeBlobP2a1Suite) verifies
616-
* the baseline state-machine; this test verifies the hook seam that feeds it.
610+
* are exercised. The manager-layer test (runWholeBlobP2a1Suite) verifies the
611+
* baseline state-machine; this test verifies the hook seam that feeds it.
617612
*
618-
* Sequence:
619-
* 1. Block bootstrap fetch (park Promise).
620-
* 2. Mount hook — bootstrap is pending.
621-
* 3. Click through the hook API (makeEdit) — publishSections(store, false)
622-
* writes v2 outbox at Date.now()/1000.
623-
* 4. Deliver H102 via subscribeLive — suppressed by hasPendingEdit.
624-
* 5. Release bootstrap with H100 → hook .then() callback fires naturally:
625-
* readOutbox → shouldReplay=true → publishSections(outbox, true, queuedAt).
626-
* 6. Fire debounce → pre-publish fetch returns H102 →
627-
* canonicalMax(publishBaseline, bootstrapResultHead=H100) = H100;
628-
* H102 is a genuine advance → ADOPT. publishCalls === 0.
613+
* Sequence: park bootstrap → mount → click (writeOutbox queuedAt=50) →
614+
* deliver H102 live (suppressed by hasPendingEdit) → release bootstrap H100 →
615+
* .then() fires shouldReplay=true → debounce → publishBaseline={30,H100};
616+
* H102 advance → ADOPT. Hook and storage reflect H102; outbox cleared.
629617
*
630618
* Causality mutation: set publishBaseline = lastRemoteHead in publish(_, true)
631-
* → publishBaseline = H102 → pre-publish sees equality → publishes over H102
632-
* → test FAILS.
619+
* → publishBaseline = H102 → pre-publish sees equality → publishes over H102.
633620
*/
634621
export function runWholeBlobP2a1HookSuite({
635622
label,
@@ -638,6 +625,7 @@ export function runWholeBlobP2a1HookSuite({
638625
useHook,
639626
makeEdit,
640627
makeRemoteStore,
628+
assertHookState,
641629
}) {
642630
test(`P2a-1 ${label} (hook): blocked-bootstrap real hook replay — H102 must be adopted as genuine advance, not published over`, async () => {
643631
const { act, cleanup, renderHook } = await import("@testing-library/react");
@@ -728,15 +716,9 @@ export function runWholeBlobP2a1HookSuite({
728716
for (let i = 0; i < 60; i++) await Promise.resolve();
729717
});
730718

731-
// Fire debounce. publish(_, true, 50):
732-
// publishBaseline = canonicalMax({0,""}, bootstrapResultHead={30,H100}) = {30,H100}.
733-
// pre-publish fetch returns H102 (createdAt=200 > 30) → remoteAdvancedSince = true.
734-
// C1 adopt-guard: remote.createdAt(200) > queuedAt(50) → ADOPT H102.
735-
// publishCalls === 0.
736-
//
737-
// Mutation: set publishBaseline = lastRemoteHead in publish(_, true)
738-
// → lastRemoteHead = H102 (hasPendingEdit kept it, but recordRemoteHead ran at liveCallback)
739-
// → publishBaseline = H102 → pre-publish H102 == baseline → no advance → PUBLISH over H102.
719+
// Fire debounce. publish(_, true, 50): publishBaseline={30,H100}; pre-publish
720+
// fetch returns H102 (200>30) → remoteAdvancedSince=true; C1 adopt-guard
721+
// (200>queuedAt=50) → ADOPT. Mutation: publishBaseline=H102 → no advance → PUBLISH.
740722
await fireDelay(2000);
741723
for (let i = 0; i < 100; i++) await Promise.resolve();
742724

@@ -770,6 +752,10 @@ export function runWholeBlobP2a1HookSuite({
770752
`remove onRemoteAdopted callback → own edit store remains persisted`,
771753
);
772754

755+
// Hook/UI state must reflect the adopted H102 store independently of storage.
756+
// Mutation (applyRemote returns prev): storage correct, React state stale → fails.
757+
assertHookState(hook.result.current, label);
758+
773759
// Own outbox written by the click must be cleared after H102 supersedes it.
774760
// Mutation (onRemoteAdopted callback removed): adopt never fires, outbox not cleared → not null → fails.
775761
assert.equal(

0 commit comments

Comments
 (0)