fix(#4778): scroll-rebuild guard must not reclassify a pinned follower as unpinned
Codex regression gate caught: the guard predicate keyed on a raw messagesEl.scrollTop>0, so a pinned reader transiently at bottomDistance>250 mid-render (assistant body grows before the anchor scene re-renders) got flipped to _messageUserUnpinned=true/_scrollPinned=false and lost auto-follow. Require an explicit non-follow signal (_messageUserUnpinned || _scrollPinned===false) instead. Updated the existing guard test to a genuine unpinned reader + added a pinned-large-growth regression. Co-authored-by: franksong2702
This commit is contained in:
1 parent
c6d633fd2c
commit
f4af6d8419
3 files changed
+53
-4
No files matched your search
+6
-1
@@ -9595,7 +9595,12 @@ function _prepareLiveAnchorScrollRebuildGuard(scrollSnapshot){
|
||||
const messagesEl=$('messages');
|
||||
if(!messagesEl||!scrollSnapshot) return {readerAwayFromBottom:false,release:null};
|
||||
const beforeBottomDistance=Math.max(0,messagesEl.scrollHeight-messagesEl.scrollTop-messagesEl.clientHeight);
|
||||
const readerAwayFromBottom=beforeBottomDistance>250&&(_messageUserUnpinned||messagesEl.scrollTop>0);
|
||||
// Only treat the reader as away if they were ALREADY in a non-follow state.
|
||||
// A pinned follower can transiently have bottomDistance>250 mid-render (the
|
||||
// assistant body grows before the anchor scene re-renders), so keying on a
|
||||
// raw scrollTop>0 here would mis-classify a pinned reader as unpinned and kill
|
||||
// auto-follow. Require an explicit unpin/non-pinned signal instead.
|
||||
const readerAwayFromBottom=beforeBottomDistance>250&&(_messageUserUnpinned||_scrollPinned===false);
|
||||
if(!readerAwayFromBottom) return {readerAwayFromBottom:false,release:null};
|
||||
scrollSnapshot.pinned=false;
|
||||
scrollSnapshot.userUnpinned=true;
|
||||
|
||||
@@ -85,7 +85,7 @@ def test_live_anchor_worklog_rebuild_guards_height_for_unpinned_reader():
|
||||
compact = _compact(guard)
|
||||
|
||||
assert "constbeforeBottomDistance=Math.max(0,messagesEl.scrollHeight-messagesEl.scrollTop-messagesEl.clientHeight);" in compact
|
||||
assert "beforeBottomDistance>250&&(_messageUserUnpinned||messagesEl.scrollTop>0)" in compact
|
||||
assert "beforeBottomDistance>250&&(_messageUserUnpinned||_scrollPinned===false)" in compact
|
||||
assert "scrollSnapshot.pinned=false;" in compact
|
||||
assert "scrollSnapshot.userUnpinned=true;" in compact
|
||||
assert "scrollSnapshot.bottom=beforeBottomDistance;" in compact
|
||||
|
||||
@@ -112,8 +112,8 @@ def test_live_anchor_rebuild_guard_holds_height_and_marks_reader_unpinned():
|
||||
|
||||
script = f"""
|
||||
const assert = require('assert');
|
||||
let _messageUserUnpinned = false;
|
||||
let _scrollPinned = true;
|
||||
let _messageUserUnpinned = true;
|
||||
let _scrollPinned = false;
|
||||
let _nearBottomCount = 2;
|
||||
const messages = {{
|
||||
scrollTop: 2000,
|
||||
@@ -165,6 +165,50 @@ assert.strictEqual(Object.prototype.hasOwnProperty.call(inner.dataset, 'liveAnch
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
|
||||
|
||||
def test_live_anchor_rebuild_guard_keeps_pinned_follower_pinned_on_large_growth():
|
||||
"""A PINNED follower must NOT be reclassified as unpinned when a large live
|
||||
render transiently pushes bottomDistance>250. Regression guard: the predicate
|
||||
must require an explicit non-follow signal (_messageUserUnpinned / _scrollPinned
|
||||
=== false), not a raw scrollTop>0, or pinned live streams stop auto-following."""
|
||||
|
||||
script = f"""
|
||||
const assert = require('assert');
|
||||
let _messageUserUnpinned = false;
|
||||
let _scrollPinned = true;
|
||||
let _nearBottomCount = 2;
|
||||
const messages = {{
|
||||
scrollTop: 4400,
|
||||
scrollHeight: 5500,
|
||||
clientHeight: 600,
|
||||
}};
|
||||
const inner = {{ style: {{ minHeight: '' }}, dataset: {{}} }};
|
||||
function $(id) {{
|
||||
if (id === 'messages') return messages;
|
||||
if (id === 'msgInner') return inner;
|
||||
return null;
|
||||
}}
|
||||
{_function_body(UI_JS, "_prepareLiveAnchorScrollRebuildGuard")}
|
||||
const snapshot = {{
|
||||
top: 4400,
|
||||
bottom: 0,
|
||||
scrollHeight: 5500,
|
||||
pinned: true,
|
||||
userUnpinned: false,
|
||||
}};
|
||||
const guard = _prepareLiveAnchorScrollRebuildGuard(snapshot);
|
||||
// bottomDistance = 5500 - 4400 - 600 = 500 (>250), but the reader is pinned and
|
||||
// has not manually unpinned -> must stay pinned, follow-scroll must NOT be skipped.
|
||||
assert.strictEqual(guard.readerAwayFromBottom, false);
|
||||
assert.strictEqual(guard.release, null);
|
||||
assert.strictEqual(_messageUserUnpinned, false);
|
||||
assert.strictEqual(_scrollPinned, true);
|
||||
assert.strictEqual(snapshot.pinned, true);
|
||||
assert.strictEqual(snapshot.userUnpinned, false);
|
||||
assert.strictEqual(inner.style.minHeight, '');
|
||||
"""
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
|
||||
|
||||
def test_restore_message_scroll_snapshot_keeps_intermediate_distance_state():
|
||||
script = f"""
|
||||
const assert = require('assert');
|
||||
|
||||
Reference in new issue
Block a user