fix(settings): honor persisted auto-follow across boot failures and partial bodies (#6856)
* fix(settings): honor persisted auto-follow across boot failures and partial bodies (#6819) The Auto-follow new content toggle was silently re-enabled by two paths: - boot.js settings-fetch-failure fallback hardcoded _autoScrollFollow=true, clobbering an explicit OFF for the whole session (post-turn scroll yanks until refresh); - _applySavedSettingsUi assigned body.auto_scroll_follow!==false unconditionally, so a partial settings body without the key evaluated undefined!==false -> true. Fix: mirror the resolved value into profile-namespaced localStorage (JSON map keyed by active profile), make the boot fallback read the mirror instead of hardcoding ON, and only override the runtime flag when a settings body actually owns the key. Fresh users (no mirror) still default to ON. Tests: update test_issue4006 fallback assertion; add test_issue6819_auto_scroll_follow_persistence covering source-shape guards and extracted-helper behavior (fresh default, persisted OFF, profile isolation, partial-body no-clobber, save-path persist). * fix(settings): resolve auto-follow mirror profile from cookie before S.activeProfile init (#6856 review) P1 (greptile-apps review): the profile-namespaced mirror was keyed by S.activeProfile, but the boot settings-fetch-failure fallback runs before boot resolves S.activeProfile, so a non-default profile with a failed fetch read the 'default' mirror entry. Resolve the key from the hermes_profile cookie (set per-client by /api/profile/switch, readable synchronously at page load) first, falling back to S.activeProfile, then 'default'. P2: document the persistence contract in the helper block (storage shape, profile-key resolution order, read semantics, upgrade path). Tests: extend the extracted-helper node test with the P1 scenario (cookie authoritative over stale S.activeProfile, per-profile isolation via cookie, no-cookie fallback) and add source-shape guards for the cookie read. * fix(settings): defer auto-follow mirror read until profile resolves (P1 follow-up) greptile-apps review: the hermes_profile cookie is HttpOnly — document.cookie can never read it, so the cookie-based profile key from 39fed70b always fell back to 'default', re-introducing the wrong-namespace bug for non-default profiles on a failed settings fetch. Correct model: the active profile is only known asynchronously via /api/profile/active. The boot settings-fetch-failure path now sets a safe default (ON) plus a deferred-reapply flag; once S.activeProfile resolves, the mirror is read with the profile-correct namespace and the flag clears. The success path (which already applied+persisted the authoritative value) never touches the flag. Tests updated to the deferred model: source-shape guard asserts the re-apply runs AFTER profile resolution (fallback < profile-resolve < reapply ordering), no document.cookie read in the profile key, and the node behavioral test simulates boot-failure -> profile-resolve -> deferred re-apply. * fix(settings): defer auto-follow mirror write until profile resolves (P1 round 3) greptile-apps review: the SUCCESS boot path called _persistAutoScrollFollow before S.activeProfile initialized, so a non-default profile's successful settings fetch stored its value in the 'default' namespace. A later failed-fetch boot would then find no entry for the real profile and restore auto-follow ON. The success path now defers the mirror write the same way the failure path defers its read: it records the value in _autoScrollFollowDeferredPersist, and the post-profile-resolution block persists it under the correct namespace (then clears the flag). Both deferred branches now run together after S.activeProfile resolves. Tests: success path must not call _persistAutoScrollFollow before profile resolution (source-shape), deferred-persist anchor ordered after profile resolve, and the node behavioral test covers success-boot -> profile-resolve -> deferred write under the resolved profile (and NOT under default). * fix(settings): make auto-follow mirror global to match the backend contract (maintainer review) Maintainer review on #6856: the mirror was profile-keyed, but the backend authority is GLOBAL — SETTINGS_FILE = STATE_DIR / 'settings.json' (api/config.py), and /api/settings calls load_settings() with no profile-specific file. A profile-keyed map leaves a freshly-opened profile with no mirror entry, so a failed settings fetch on that profile wrongly restored ON despite the global OFF — the original failure stayed reproducible. Revert to a single global last-known value ('1'/'0' under one key): - _persistAutoScrollFollow / _readPersistedAutoScrollFollow now use a plain scalar, not a JSON profile map. - Boot success path persists synchronously (no deferred write — the global mirror has no profile dimension). - Boot failure fallback reads synchronously (no deferred re-apply — no profile resolution needed). - Remove _autoScrollFollowProfileKey, _autoScrollFollowDeferredPersist, _autoScrollFollowDeferredReapply entirely. Tests: behavioral node test now asserts the maintainer-requested regression (global OFF survives profile change + failed fetch, single scalar storage); source-shape guards assert the profile map and deferred machinery are gone; boot success persists; partial-body and save-path assertions unchanged. 14/14 pass. * fix(settings): never persist synthesized auto-follow from a failed autosave (Greptile P1) Greptile review on #6856: in _autosaveAppearanceSettings, when the POST fails (saved is falsy), '!saved' synthesizes _autoScrollFollow=true, and the added persist call wrote that synthesized ON into the mirror. A later boot settings-fetch failure would then restore ON from the corrupted mirror, reintroducing post-turn scroll yanks for a user who explicitly disabled auto-follow. Persist ONLY from an explicit boolean in the server response: if(saved && typeof saved.auto_scroll_follow==='boolean' && typeof _persistAutoScrollFollow==='function'){ _persistAutoScrollFollow(saved.auto_scroll_follow); } A failed save or a partial response without the key leaves the mirror untouched (last authoritative value preserved). Tests: source-shape guard asserts the explicit-boolean requirement; the node behavioral test replicates the failed-autosave path (mirror stays OFF) and the successful-autosave path (explicit boolean persists). * fix(settings): sync auto-follow mirror on successful settings-panel GET Gate review (Codex) found a stale-mirror gap in #6856's own contract: a successful loadSettingsPanel() GET applied the authoritative auto_scroll_follow value to window._autoScrollFollow but did not persist the global mirror, so an explicit OFF applied via the panel left a stale ON mirror that a later boot-fetch failure would restore (reopening #6819). Persist from the panel GET too, only when the server sent an explicit boolean (never a synthesized default) — matching the boot + autosave contract. Adds a bite-tested regression. Co-authored-by: sand01chi <sand01chi@users.noreply.github.com> --------- Co-authored-by: sand01chi <sand01chi@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
4 files changed
+272
-8
No files matched your search
+52
-2
@@ -1336,6 +1336,47 @@ const _DEFAULT_MESSAGE_MODES=['queue','interrupt','steer'];
|
||||
// existing user's persisted busy-input-mode preference survives the rename.
|
||||
const _LEGACY_DEFAULT_MESSAGE_MODE_KEY='hermes-busy-input-mode';
|
||||
const _DEFAULT_MESSAGE_MODE_KEY='hermes-default-message-mode';
|
||||
// ── Auto-follow eager mirror (#6819) ────────────────────────────────────────
|
||||
// PERSISTENCE CONTRACT (client-side mirror of the Auto-follow new content
|
||||
// setting, `auto_scroll_follow`):
|
||||
//
|
||||
// * Why: the boot settings-fetch-failure path used to hardcode
|
||||
// `window._autoScrollFollow=true`, silently clobbering an explicit OFF for
|
||||
// the whole session (post-turn scroll yanks until the next refresh).
|
||||
// * Storage: ONE global localStorage value (`'1'`/`'0'`) under
|
||||
// `_AUTO_SCROLL_FOLLOW_KEY` — NOT profile-keyed. The backend authority is
|
||||
// global: `SETTINGS_FILE = STATE_DIR / "settings.json"` (api/config.py)
|
||||
// and `/api/settings` calls `load_settings()` with no profile-specific
|
||||
// file, so the mirror must match that single global contract. A
|
||||
// profile-keyed map would leave a freshly-opened profile with no entry and
|
||||
// wrongly fall back to ON despite the global OFF (maintainer review on
|
||||
// #6856).
|
||||
// * Write semantics: written ONLY when a settings response (or the boot
|
||||
// settings path) actually resolves the setting; the fallback path never
|
||||
// writes.
|
||||
// * Read semantics: `_readPersistedAutoScrollFollow()` returns the stored
|
||||
// value if present, else `true` (the config.py default). The boot-failure
|
||||
// path reads it directly (no profile resolution needed — the mirror is
|
||||
// global, so it can be read synchronously in the fallback). Fresh users
|
||||
// (no mirror) get ON.
|
||||
// * Upgrade: there is no legacy key; the mirror is created on first settings
|
||||
// resolve, so older sessions simply default to ON until the next settings
|
||||
// round-trip writes it.
|
||||
const _AUTO_SCROLL_FOLLOW_KEY='hermes-auto-scroll-follow';
|
||||
function _persistAutoScrollFollow(enabled){
|
||||
try{localStorage.setItem(_AUTO_SCROLL_FOLLOW_KEY,enabled?'1':'0');}catch(_){}
|
||||
return enabled;
|
||||
}
|
||||
function _readPersistedAutoScrollFollow(){
|
||||
try{
|
||||
const raw=localStorage.getItem(_AUTO_SCROLL_FOLLOW_KEY);
|
||||
if(raw==='1') return true;
|
||||
if(raw==='0') return false;
|
||||
}catch(_){}
|
||||
return true; // default: follow ON (matches config.py default)
|
||||
}
|
||||
window._persistAutoScrollFollow=_persistAutoScrollFollow;
|
||||
window._readPersistedAutoScrollFollow=_readPersistedAutoScrollFollow;
|
||||
function _normalizeDefaultMessageMode(mode){
|
||||
return _DEFAULT_MESSAGE_MODES.includes(mode)?mode:'steer';
|
||||
}
|
||||
@@ -3286,7 +3327,11 @@ window._mirrorSpeechSettingsFromServer=_mirrorSpeechSettingsFromServer;
|
||||
window._showBusyPlaceholderHint=!!s.show_busy_placeholder_hint;
|
||||
window._newChatOnWorkspaceSwitch=!!s.new_chat_on_workspace_switch; // #5473 opt-in
|
||||
window._sessionEndlessScrollEnabled=!!s.session_endless_scroll;
|
||||
window._autoScrollFollow=s.auto_scroll_follow!==false;
|
||||
// #6819: persist the resolved auto-follow value into the global mirror.
|
||||
// The mirror is NOT profile-keyed (the backend setting is one global
|
||||
// settings.json), so it can be written synchronously here — no deferred
|
||||
// write needed.
|
||||
window._autoScrollFollow=_persistAutoScrollFollow(s.auto_scroll_follow!==false);
|
||||
window._largeTextPasteAsAttachment=s.large_text_paste_as_attachment!==false;
|
||||
window._projectQuickCreate=!!s.project_quick_create_buttons;
|
||||
window._composerControlVisibility=_composerControlVisibilityFromSettings(s);
|
||||
@@ -3428,7 +3473,12 @@ window._mirrorSpeechSettingsFromServer=_mirrorSpeechSettingsFromServer;
|
||||
window._defaultMessageMode=_readPersistedDefaultMessageMode();
|
||||
window._showBusyPlaceholderHint=false;
|
||||
window._sessionEndlessScrollEnabled=false;
|
||||
window._autoScrollFollow=true;
|
||||
// #6819: honor the persisted auto-follow preference on settings-fetch
|
||||
// failure instead of hardcoding ON (which silently clobbered an explicit
|
||||
// OFF and caused post-turn scroll yanks until the next refresh). The
|
||||
// mirror is global (matches the one global settings.json), so it is read
|
||||
// synchronously here — no profile resolution or deferral needed.
|
||||
window._autoScrollFollow=_readPersistedAutoScrollFollow();
|
||||
window._composerControlVisibility=_composerControlVisibilityFromSettings(null);
|
||||
window._composerControlOrder=[];
|
||||
_applyComposerControlOrder(window._composerControlOrder);
|
||||
|
||||
@@ -8542,6 +8542,14 @@ async function _autosaveAppearanceSettings(payload){
|
||||
}
|
||||
window._sessionEndlessScrollEnabled=!!(saved&&saved.session_endless_scroll);
|
||||
window._autoScrollFollow=!saved||saved.auto_scroll_follow!==false;
|
||||
// #6819: persist ONLY from an explicit boolean in the server response.
|
||||
// A failed autosave (`saved` falsy) or a partial response without the key
|
||||
// would otherwise write the synthesized default (ON) into the mirror,
|
||||
// corrupting the value a later boot-failure fallback would restore
|
||||
// (Greptile P1 review on #6856).
|
||||
if(saved&&typeof saved.auto_scroll_follow==='boolean'&&typeof _persistAutoScrollFollow==='function'){
|
||||
_persistAutoScrollFollow(saved.auto_scroll_follow);
|
||||
}
|
||||
window._largeTextPasteAsAttachment=!saved||saved.large_text_paste_as_attachment!==false;
|
||||
window._projectQuickCreate=!!(saved&&saved.project_quick_create_buttons);
|
||||
if(saved&&Object.prototype.hasOwnProperty.call(saved,'structured_code_default_view')){
|
||||
@@ -9051,6 +9059,14 @@ async function loadSettingsPanel(){
|
||||
if(autoScrollFollowCb){
|
||||
autoScrollFollowCb.checked=settings.auto_scroll_follow!==false;
|
||||
window._autoScrollFollow=autoScrollFollowCb.checked;
|
||||
// #6819/#6856: a successful settings GET is an authoritative resolve, so
|
||||
// sync the global mirror too — otherwise an explicit OFF applied here
|
||||
// leaves a stale ON mirror that a later boot-fetch failure would restore.
|
||||
// Persist ONLY when the server explicitly sent a boolean (never persist a
|
||||
// synthesized default from an absent field — matches the boot contract).
|
||||
if(typeof settings.auto_scroll_follow==='boolean'&&typeof _persistAutoScrollFollow==='function'){
|
||||
_persistAutoScrollFollow(settings.auto_scroll_follow);
|
||||
}
|
||||
autoScrollFollowCb.onchange=function(){
|
||||
window._autoScrollFollow=this.checked;
|
||||
_scheduleAppearanceAutosave();
|
||||
@@ -11957,7 +11973,13 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
? _persistDefaultMessageMode(body.default_message_mode||body.busy_input_mode)
|
||||
: (body.default_message_mode||body.busy_input_mode||'steer');
|
||||
window._sessionEndlessScrollEnabled=!!body.session_endless_scroll;
|
||||
// #6819: only override auto-follow when the body actually carries the key.
|
||||
// A partial settings body without it must not silently re-enable follow
|
||||
// (`undefined !== false` evaluates true — the old clobber).
|
||||
if(Object.prototype.hasOwnProperty.call(body,'auto_scroll_follow')){
|
||||
window._autoScrollFollow=body.auto_scroll_follow!==false;
|
||||
if(typeof _persistAutoScrollFollow==='function') _persistAutoScrollFollow(window._autoScrollFollow);
|
||||
}
|
||||
window._largeTextPasteAsAttachment=body.large_text_paste_as_attachment!==false;
|
||||
window._projectQuickCreate=!!body.project_quick_create_buttons;
|
||||
if(Object.prototype.hasOwnProperty.call(body,'structured_code_default_view')){
|
||||
|
||||
@@ -45,17 +45,23 @@ def test_boot_hydration_defaults_true_when_setting_absent():
|
||||
omit the key — `!!s.auto_scroll_follow` would wrongly default it OFF for
|
||||
every existing user, contradicting the config.py default."""
|
||||
src = _read("static/boot.js")
|
||||
# Settings path: default-true read (=== false), not the truthy-coerce form.
|
||||
assert "window._autoScrollFollow=s.auto_scroll_follow!==false" in src, (
|
||||
"boot.js settings path must default _autoScrollFollow True when absent"
|
||||
# Settings path: default-true read (=== false), not the truthy-coerce form,
|
||||
# and the resolved value is persisted into the #6819 mirror (global —
|
||||
# matches the one global settings.json, per maintainer review on #6856).
|
||||
assert "window._autoScrollFollow=_persistAutoScrollFollow(s.auto_scroll_follow!==false)" in src, (
|
||||
"boot.js settings path must default _autoScrollFollow True when absent "
|
||||
"(and persist the resolved value into the #6819 mirror)"
|
||||
)
|
||||
assert "window._autoScrollFollow=!!s.auto_scroll_follow" not in src, (
|
||||
"boot.js must not use !!s.auto_scroll_follow — that defaults the True "
|
||||
"setting OFF for users with no saved value"
|
||||
)
|
||||
# Fallback (no-settings) path must also default true.
|
||||
assert "window._autoScrollFollow=true" in src, (
|
||||
"boot.js fallback path must default _autoScrollFollow to true"
|
||||
# Fallback (no-settings) path must also default true for a fresh user
|
||||
# (no persisted mirror), while honoring a persisted OFF (#6819).
|
||||
assert "window._autoScrollFollow=_readPersistedAutoScrollFollow()" in src, (
|
||||
"boot.js fallback path must read the persisted auto-follow mirror "
|
||||
"(defaults true for fresh users, honors saved OFF after a transient "
|
||||
"settings-fetch failure)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Regression coverage for #6819 — the Auto-follow toggle must survive
|
||||
transient settings-fetch failures and partial settings bodies.
|
||||
|
||||
Root cause fixed: the boot settings-fetch-failure path hardcoded
|
||||
`window._autoScrollFollow=true`, silently clobbering an explicit OFF for the
|
||||
whole session (post-turn scroll yanks until refresh), and
|
||||
`_applySavedSettingsUi` assigned `body.auto_scroll_follow!==false`
|
||||
unconditionally — a partial body without the key evaluates `undefined!==false`
|
||||
→ `true`, re-enabling follow mid-session.
|
||||
|
||||
The fix mirrors the resolved value into GLOBAL localStorage (the backend
|
||||
setting is one global settings.json — a profile-keyed map would leave a
|
||||
freshly-opened profile with no entry and wrongly restore ON, per maintainer
|
||||
review on #6856), makes the boot fallback read the mirror instead of
|
||||
hardcoding ON, and only overrides the runtime flag when a settings body
|
||||
actually owns the key.
|
||||
"""
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _read(rel):
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ── Source-shape guards (what the code must contain) ────────────────────────
|
||||
|
||||
def test_boot_has_global_mirror_helpers():
|
||||
src = _read("static/boot.js")
|
||||
assert "_persistAutoScrollFollow" in src, "persist helper must exist"
|
||||
assert "_readPersistedAutoScrollFollow" in src, "read helper must exist"
|
||||
assert "_AUTO_SCROLL_FOLLOW_KEY" in src, "storage key constant must exist"
|
||||
# Maintainer review (#6856): the backend setting is GLOBAL (one
|
||||
# settings.json), so the mirror must NOT be profile-keyed — a profile map
|
||||
# would leave a freshly-opened profile with no entry and wrongly restore
|
||||
# ON. The helper must be a single global scalar, read synchronously in
|
||||
# the boot fallback (no profile resolution, no deferral).
|
||||
assert "_autoScrollFollowProfileKey" not in src, (
|
||||
"mirror must NOT be profile-keyed (maintainer review, #6856)"
|
||||
)
|
||||
assert "_autoScrollFollowDeferredReapply" not in src, (
|
||||
"no deferred re-apply needed — global mirror is read synchronously"
|
||||
)
|
||||
assert "_autoScrollFollowDeferredPersist" not in src, (
|
||||
"no deferred persist needed — global mirror is written synchronously"
|
||||
)
|
||||
|
||||
|
||||
def test_boot_success_path_persists_mirror():
|
||||
src = _read("static/boot.js")
|
||||
assert "window._autoScrollFollow=_persistAutoScrollFollow(" in src, (
|
||||
"boot settings path must persist the resolved value into the mirror"
|
||||
)
|
||||
|
||||
|
||||
def test_boot_fallback_reads_mirror_not_hardcoded_true():
|
||||
src = _read("static/boot.js")
|
||||
assert "window._autoScrollFollow=_readPersistedAutoScrollFollow()" in src, (
|
||||
"the boot-failure fallback must read the mirror synchronously"
|
||||
)
|
||||
# The old hardcoded fallback must be gone (ui.js keeps its own
|
||||
# undefined-before-init default at _autoScrollFollow===undefined, #6614 —
|
||||
# a different, legitimate case not touched by #6819).
|
||||
assert "_sessionEndlessScrollEnabled=false;" in src, (
|
||||
"fallback anchor missing"
|
||||
)
|
||||
|
||||
|
||||
def test_partial_body_does_not_override_follow():
|
||||
src = _read("static/panels.js")
|
||||
assert (
|
||||
"hasOwnProperty.call(body,'auto_scroll_follow')" in src
|
||||
or "hasOwnProperty.call(body, 'auto_scroll_follow')" in src
|
||||
), (
|
||||
"_applySavedSettingsUi must guard the override with hasOwnProperty so "
|
||||
"a partial body without the key cannot silently re-enable follow"
|
||||
)
|
||||
|
||||
|
||||
def test_settings_save_persists_mirror():
|
||||
src = _read("static/panels.js")
|
||||
# Greptile P1 (#6856): the autosave path must persist ONLY from an
|
||||
# explicit boolean in the server response — a failed save (`saved` falsy)
|
||||
# or a response without the key must not write the synthesized default
|
||||
# (ON) into the mirror.
|
||||
assert "_persistAutoScrollFollow(saved.auto_scroll_follow)" in src, (
|
||||
"autosave must persist from the explicit server boolean"
|
||||
)
|
||||
assert "typeof saved.auto_scroll_follow==='boolean'" in src, (
|
||||
"autosave must require an explicit boolean before persisting (P1)"
|
||||
)
|
||||
|
||||
|
||||
def test_settings_panel_load_persists_mirror():
|
||||
src = _read("static/panels.js")
|
||||
# Regression (#6856 gate finding): a successful settings-panel GET applies
|
||||
# the authoritative value to window._autoScrollFollow — it must ALSO sync the
|
||||
# global mirror, or an explicit OFF applied here leaves a stale ON mirror that
|
||||
# a later boot-fetch failure restores (reopening #6819). Persist only from an
|
||||
# explicit server boolean (never a synthesized default), matching the contract.
|
||||
panel_block = src.split("const autoScrollFollowCb=$('settingsAutoScrollFollow');", 1)[1][:900]
|
||||
assert "typeof settings.auto_scroll_follow==='boolean'" in panel_block, (
|
||||
"settings-panel load must require an explicit boolean before persisting"
|
||||
)
|
||||
assert "_persistAutoScrollFollow(settings.auto_scroll_follow)" in panel_block, (
|
||||
"settings-panel load must sync the global mirror from the authoritative GET value"
|
||||
)
|
||||
|
||||
|
||||
# ── Behavioral guards (extracted helper logic must behave correctly) ────────
|
||||
|
||||
def test_mirror_helpers_behavior_via_source_extraction():
|
||||
"""Extract the real JS helper functions from boot.js and execute them under
|
||||
node with a fake localStorage to prove the semantics end-to-end."""
|
||||
import re as _re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
assert shutil.which("node"), "node required for this test"
|
||||
src = _read("static/boot.js")
|
||||
m = _re.search(
|
||||
r"const _AUTO_SCROLL_FOLLOW_KEY=.*?window\._readPersistedAutoScrollFollow=_readPersistedAutoScrollFollow;",
|
||||
src,
|
||||
_re.S,
|
||||
)
|
||||
assert m, "helper block not found in boot.js"
|
||||
block = m.group(0)
|
||||
|
||||
js = r"""
|
||||
const store = {};
|
||||
const localStorage = {
|
||||
getItem(k){ return (k in store) ? store[k] : null; },
|
||||
setItem(k,v){ store[k] = v; }
|
||||
};
|
||||
const window = {};
|
||||
""" + block + r"""
|
||||
// 1. fresh user: default ON
|
||||
if (_readPersistedAutoScrollFollow() !== true) throw new Error('fresh default must be ON');
|
||||
// 2. user turns OFF -> persisted (global)
|
||||
if (_persistAutoScrollFollow(false) !== false) throw new Error('persist OFF failed');
|
||||
if (_readPersistedAutoScrollFollow() !== false) throw new Error('persisted OFF must be honored');
|
||||
// 3. MAINTAINER-REVIEW REGRESSION (#6856): the backend setting is GLOBAL
|
||||
// (one settings.json), so the mirror must be too. Saving OFF under one
|
||||
// profile, then a failed settings fetch on ANY other profile, must still
|
||||
// restore OFF — a profile-keyed map would find no entry and wrongly
|
||||
// return ON. With the global mirror there is no profile dimension at all:
|
||||
// simulate the boot-failure path directly.
|
||||
const bootFailureFallbackValue = _readPersistedAutoScrollFollow();
|
||||
if (bootFailureFallbackValue !== false) throw new Error('global mirror must survive profile change + failed fetch (maintainer review)');
|
||||
// 4. ON round-trip
|
||||
if (_persistAutoScrollFollow(true) !== true) throw new Error('persist ON failed');
|
||||
if (_readPersistedAutoScrollFollow() !== true) throw new Error('persisted ON must be honored');
|
||||
// 5. GREPTILE P1 (#6856): a FAILED autosave (`saved` falsy) must NOT write
|
||||
// the synthesized default into the mirror — replicate the autosave guard:
|
||||
// only an explicit boolean from the server response persists.
|
||||
// (a) mirror holds OFF (user's real preference)
|
||||
if (_persistAutoScrollFollow(false) !== false) throw new Error('setup OFF failed');
|
||||
// (b) failed autosave: saved=null -> guard rejects -> mirror stays OFF
|
||||
const savedFailed = null;
|
||||
if (savedFailed && typeof savedFailed.auto_scroll_follow === 'boolean' && typeof _persistAutoScrollFollow === 'function') {
|
||||
_persistAutoScrollFollow(savedFailed.auto_scroll_follow);
|
||||
}
|
||||
if (_readPersistedAutoScrollFollow() !== false) throw new Error('failed autosave must NOT corrupt the mirror (Greptile P1)');
|
||||
// (c) successful autosave with explicit boolean -> mirror updates
|
||||
const savedOk = { auto_scroll_follow: true };
|
||||
if (savedOk && typeof savedOk.auto_scroll_follow === 'boolean' && typeof _persistAutoScrollFollow === 'function') {
|
||||
_persistAutoScrollFollow(savedOk.auto_scroll_follow);
|
||||
}
|
||||
if (_readPersistedAutoScrollFollow() !== true) throw new Error('successful autosave must persist the explicit boolean');
|
||||
// 6. storage is a single global scalar, not a profile map
|
||||
if (store['hermes-auto-scroll-follow'] !== '1') throw new Error('storage must be a single scalar value: '+store['hermes-auto-scroll-follow']);
|
||||
console.log('MIRROR-HELPERS-OK');
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
["node", "-e", js],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=ROOT,
|
||||
timeout=30,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert "MIRROR-HELPERS-OK" in proc.stdout, proc.stdout
|
||||
Reference in new issue
Block a user