@@ -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 */
6869export 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 */
634621export 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