feat(deploy): version-coherence — WS-анонс версии + guarded reload устаревших вкладок (#481) #499
Open
agent_coder
wants to merge 3 commits from
feat/481-version-coherence into develop
pull from: feat/481-version-coherence
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:fix/486-iter1-prod-fires
vvzvlad:develop
vvzvlad:refactor/347-client-md-paste
vvzvlad:perf/479-getpage-cache
vvzvlad:refactor/449-write-invariants
vvzvlad:refactor/450-split-client
vvzvlad:ci/476-publish-gates
vvzvlad:feat/443-get-page-context
vvzvlad:feat/443-get-tree
vvzvlad:feat/443-search
vvzvlad:docs/415-lossy-descriptions
vvzvlad:feat/413-markdown-default
vvzvlad:refactor/412-camelcase-tool-names
vvzvlad:feat/425-drawio-graph
vvzvlad:fix/464-diffdocs-cpu-guard
vvzvlad:refactor/411-update-page-markdown
vvzvlad:feat/424-drawio-rules
vvzvlad:fix/409-invalid-node-validation
vvzvlad:refactor/448-generate-inventory
vvzvlad:refactor/445-execute-mapping
vvzvlad:refactor/446-derive-client-types
vvzvlad:fix/447-registry-stamp-ci
vvzvlad:fix/444-agent-loop-guards
vvzvlad:fix/396-sendnow-detached-run
vvzvlad:fix/452-immutable-cache-control
vvzvlad:docs/footnote-authoring-comment-cleanup
vvzvlad:feat/437-error-diagnostics
vvzvlad:fix/442-cursor-pagination
vvzvlad:feat/419-footnote-normalize
vvzvlad:perf/399-comment-resolve-async
vvzvlad:perf/435-collab-token-cache
vvzvlad:perf/344-background-rerenders
vvzvlad:perf/348-backend-lowhanging
vvzvlad:feat/423-drawio-crud
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/420-footnote-render
vvzvlad:dummy-review/mcp-hang-fix
vvzvlad:dummy-review/mcp-hang-base
vvzvlad:feat/381-resumable-sse-pr1
vvzvlad:feat/git-sync-2
vvzvlad:docs/agents-workspace-build-order
vvzvlad:feature/offline-sync
vvzvlad:perf/342-code-splitting
vvzvlad:perf/346-compression-cache
vvzvlad:feat/196-multi-cursor
vvzvlad:feat/370-page-versioning
vvzvlad:perf/343-typing-latency
vvzvlad:docs/how-to-test
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#499
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/481-version-coherence"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
SPA держат открытым часами; сервер за это время передеплоивают → старый
index.htmlвкладки ссылается на content-hashed чанки, которых в новом образе нет → ленивыйimport()→ catch-all отдаётindex.htmlкакtext/html→ WebKit «not a valid JavaScript MIME type» (наблюдали в проде). Сейчас предохранитель только реактивный (ловит уже случившуюся ошибку чанка). Делаем сигнал проактивным: сервер сообщает версию по существующему WS, клиент сравнивает со вшитой и делает guarded reload ДО битого чанка. UX (выбор владельца): баннер + reload в фоне, видимую вкладку не дёргаем. closes #481Единый источник версии
vite.config.tsвычисляетresolveAppVersionРОВНО раз → и вdefine.APP_VERSION, и в плагине, пишущемdist/version.json. Бандл и файл тождественны by construction. Сервер читаетclient/dist/version.jsonпри старте (client-version.ts;resolveClientDistPathвынесен из static.module — единый источник пути; ошибка чтения →'', fail-safe). Никаких Docker/ENV-изменений.WS-контракт + доставка
Отдельное socket.io-событие
app-version {version}(НЕ вmessage/WebSocketEvent).ws.gateway: версия читается вonModuleInit+ boot-лог ACTIVE/DISABLED;emitвhandleConnection(success-ветка). Только per-connect анонс, БЕЗ broadcast (broadcast в кластере → thundering-herd reload всего флота; естественный реконнект после редеплоя покрывает и single-container, и кластер).Клиент — guarded reload
Listener навешан СИНХРОННО в том же
useEffect, что создаёт сокет (до connect — иначе гонка с немедленным emit). ЧистаяdecideVersionAction(reload|banner|noop; пустая версия → noop fail-safe). Грязная оболочка:hidden→немедленный reload,visible→баннер + reload при уходе в фон. Общий one-shot флаг (reload-guard.tsнад существующимchunk-reload-attempted; boundary переведён на него): проактивный + реактивный reload делят ОДИН счётчик → максимум одна авто-перезагрузка за сессию; permanent skew / oscillation / disabled storage → после первого reload только баннер, петли нет.How verified
client-version7/7; client vitestversion-coherence+reload-guard+guarded-reload16/16 (coverage 97.9%).APP_VERSION=test-A vite build→dist/version.json={"version":"test-A"}+test-Aвшито в бандл.⚠️ Требует staging-проверки (вне автостенда) — шаги приёмки #481
docker build --build-arg APP_VERSION=test-A:cat dist/version.json={"version":"test-A"}; шапка=test-A; DevTools WS-кадрapp-version=test-A— три совпадают.test-B,docker compose up -d→ в секунды после реконнекта: видимая вкладка показывает баннер, фоновая перезагружается; DevTools: новый connect +app-versiontest-B.APP_VERSION==test-B.chunk-reload-attemptedвыставлен), дальше баннер; петли нет.Checklist
Готово к ревью. Проактивная version-coherence: сервер анонсирует версию по WS per-connect, клиент делает guarded reload устаревших вкладок до битого чанка. Единый источник (version.json == бандл), максимум один авто-reload за сессию (общий флаг с реактивным boundary), петле-безопасно.
Внутренний цикл: 1 проход (адверсарно, фокус на два опасных сценария). (1) Петля reload прослежена исчерпывающе: permanent skew (клиент навсегда A, сервер B) даёт РОВНО один reload, дальше только баннер; disabled-storage → авто-reload вообще не вызывается; oscillation нод → после первого reload баннер. (2) Гонка emit↔listener закрыта на connect И reconnect (listener навешан синхронно до connect; socket.io хранит обработчики между reconnect — реконнект и есть механизм доставки после редеплоя). Path-хелпер, decideVersionAction (4 ветки), visibility, scope (без broadcast, не в WebSocketEvent) — чисто. Блокеров нет; единственный nit — не покрыта тестом строка onClick ручной кнопки (зовёт уже протестированный performAutoReload).
Staging-приём (docker build-arg, WS-кадры в DevTools, мульти-таб visible/hidden) — вне автостенда, шаги в теле PR (крит.1-4). Прошу прогнать на стенде.
Ревью #499 — version-coherence (авто-перезагрузка устаревших вкладок). Вердикт: needs-human (нужно решение владельца)
Фича сделана крепко и безопасно: анти-петля перезагрузок держится на всех путях (проверил исчерпывающе), версия клиента и сервера тождественны by construction (подтвердил реальной сборкой), auth не обходится, race «слушатель до connect» закрыт. Корректных багов нет, объективка зелёная. Но один вопрос — продуктовый, и он твой: выбранное поведение «перезагружать скрытую вкладку в фоне» молча теряет несохранённый ввод (черновик комментария, незакрытая форма) — а это не контент страницы (он-то переживает перезагрузку).
Петля/лейблы: ставлю
needs-human, снимаюreview/needs. Кодер и я ждём твоего ответа. После — раскатаю в задачи (F1, F2 ниже) в любом случае.🙋 Решение, которое нужно от тебя
Что происходит. После того как в течение сессии произошёл редеплой (то есть ровно сценарий, ради которого фича и делается), клиент знает, что версия устарела. Ты выбрал: видимую вкладку не дёргать, показать баннер; а вкладку, которая ушла в фон, — молча перезагрузить («никто не смотрит»). Проблема в том, что «никто не смотрит» ≠ «нет несохранённой работы»: пользователь мог начать писать комментарий и переключиться на другую вкладку (скопировать ссылку, глянуть почту). В этот момент вкладка становится скрытой → срабатывает фоновая перезагрузка → черновик комментария/незаполненная форма пропадают без предупреждения (нет диалога «несохранённые изменения», нет спасения черновика). Контент самой страницы НЕ теряется — редактор синхронит в IndexedDB и переживает reload; страдает только ввод вне редактора (комментарии, настройки, share-форма).
Почему спрашиваю тебя, а не решаю сам. До этого PR переключение вкладки НИКОГДА не перезагружало страницу — это новая поверхность потери данных. Убрать потерю можно тремя разными способами с разной ценой, и они меняют выбранный тобой UX. «Никогда молча не терять несохранённое» — принцип почти всегда верный, но здесь он борется с простотой твоего решения, и размен (редкий короткий кейс vs усложнение) — твой.
Рекомендация: если черновики для тебя реальный кейс — C (дёшево и данные целы, скрытую вкладку просто не трогаем, а перезагружаем на следующем клике-переходе; реактивный ловец битых чанков остаётся бэкапом). B — если хочешь именно фоновый reload и готов на усложнение с детекцией «грязного» состояния. A — только если считаешь, что окно (редеплой + недописанный коммент + уход со вкладки) достаточно редкое, чтобы принять потерю.
✅ Что проверено (объективка + верификация)
client-version7/7; клиентский vitestversion-coherence+reload-guard+guarded-reload16/16; клиентская сборка ок; version.json ↔ APP_VERSION тождественны эмпирически —version.json={"version":"v0.94.1-540-g1e492500"}и эта же строка вшита в бандл (2 вхождения). Значит match/mismatch осмысленный, ложного reload на каждый деплой нет.import.metaвpackages/mcp/src/lib/drawio-{presets,shapes}.ts(проблема #472, не CI-чек — CI гоняет jest, неtsc --noEmit). Файлы #499 (ws.gateway/client-version/static.module) компилируются чисто.sessionStorage-флагchunk-reload-attempted; mark-ПЕРЕД-reload (write-fail→баннер, не reload); read-error→«уже перезагружались» (fail toward НЕ reload). Максимум ОДНА авто-перезагрузка за сессию. Перм-скью/oscillation/disabled storage — петли нет. Тест реально спаитwindow.location.reloadи ассертит «НЕ вызван на 2-м mismatch».app-version— в success-ветке после verifyJwt (неавторизованный не получит), только{version}, без broadcast; version.json читается__dirname-anchored (без traversal), ошибка→''fail-safe; версия в DOM не рендерится (XSS нет); секретов нет.resolveClientDistPathрезолвит идентично (раздача SPA не сломана), emit аддитивен,define.APP_VERSIONтолько вынесен (без chunk-churn), слушатель без утечки. Coherence: изменение строго ≥ статус-кво (реактивный предохранитель — бэкап для вкладок без реконнекта).🔧 Сделать (в любом случае, после решения по вопросу выше)
guarded-reload.tsx:98-110— веткаreloadничего не пишет передwindow.location.reload(), а reload стирает консоль. Полевой репорт «вкладка сама перезагрузилась/циклит» недиагностируем: не видно, какая пара версий сработала и какой путь (проактивный vs ловец чанков). Фикс: писать хлебную крошку в sessionStorage (server/client версии + timestamp + путь) прямо передperformAutoReload, показывать/логировать после reload.version.json(эмитится vite из того жеappVersion, читается ws.gateway).⛔ DROP — кодеру НЕ делать · калибровка (оператору)
[below-threshold]low/high[security] Не-строковый payloadapp-version→.trimбросит TypeError в socket-листенере. Эмитит только доверенный сервер (всегда строка) → на практике недостижимо; defense-in-depth. Дешёвыйtypeof-гард опционален.[below-threshold/intended]low/high[regressions] Проактивный reload может «потратить» one-shot, который нужен реактивному ловцу чанков → позднейший битый чанк даст полноэкранную кнопку Reload (не тост, юзер НЕ застрял). Это и есть намеренный анти-oscillation гард.[below-threshold]low/med[test-coverage] Нет build-assert на version.json↔APP_VERSION — by construction из одной переменной; тест был быx===x.[below-threshold/intended]suggestion/med[simplification] version.json можно писать через RollupemitFileвместо ручногоwriteBundle+fs — но текущая форма намеренно держит файл вне compression-пайплайна; выбор автора.9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence) + гейт с реальной клиентской сборкой и проверкой version.json↔APP_VERSION. security/regressions/architecture/coherence/conventions — чисто; анти-петля исчерпывающе безопасна. Эскалация — не про баг в коде, а про продуктовый размен по потере несохранённого ввода, который твой.
Решение владельца получено — вариант C. Вердикт: changes-requested
Владелец выбрал C по вопросу потери несохранённого ввода: не перезагружать скрытую вкладку по
visibilitychange; вместо этого — баннер + авто-reload на следующем переходе внутри приложения. Обоснование: навигация внутри SPA и так сносит несохранённое component-state текущей страницы, поэтому reload ровно в этот момент не добавляет потери данных сверх той, что уже вызывает сам переход; реактивный ловец битых чанков остаётся бэкапом для вкладки, которая не навигейтит и не реконнектит.needs-human снят. Открыто: F1 (перевести reload на навигацию), F2 (след в логах перед reload), F3 (CHANGELOG).
🔧 Сделать, потом ре-ревью
F1 [stability] Заменить reload-по-visibilitychange на reload-по-навигации (вариант C владельца).
apps/client/src/features/user/guarded-reload.tsx:98-110.document.addEventListener("visibilitychange", …)→performAutoReloadи немедленный reload скрытой-при-получении вкладки.action === "reload"): показать баннер (кнопка Update реального reload'а немедленно — как сейчас, под тем же one-shot гардом) И повесить одноразовый хук на следующий переход роутера внутри приложения, который вызываетperformAutoReload()(тот же shared one-shot: mark-перед-reload, при spent/непишущемся storage — только баннер). Так вкладка перезагрузится в безопасной точке (пользователь и так уходит со страницы), а не когда он просто свернул её с недописанным комментарием.chunk-load-error-boundaryне трогать — остаётся бэкапом (полноэкранная кнопка Reload) для вкладки, которая ни разу не навигейтит и не реконнектит.guarded-reload.test.tsx: вместоvisibilitychange→reload — драйвить навигацию роутера и ассертить, чтоwindow.location.reloadвызывается РОВНО раз на первом переходе после mismatch, НЕ вызывается на 2-м mismatch (one-shot), и НЕ вызывается просто от ухода вкладки в фон.F2 [stability] След перед авто-перезагрузкой.
guarded-reload.tsx— писать хлебную крошку в sessionStorage (server/client версии + timestamp + путь: proactive vs chunk-boundary) прямо передperformAutoReload, показывать/логировать после reload — иначе «вкладка сама перезагрузилась» недиагностируема (reload стирает консоль).F3 [documentation] CHANGELOG + AGENTS.md. Запись в
CHANGELOG.md [Unreleased] Addedпро наблюдаемое поведение (баннер «доступна новая версия» + авто-reload на навигации после деплоя). Строка вAGENTS.mdversion-секции про артефактclient/dist/version.json(эмитится vite из того жеappVersion, читается ws.gateway для анонса версии).Полный отчёт по 9 аспектам + объективка (гейт зелёный: client-version 7/7, client vitest 16/16, сборка + version.json↔APP_VERSION проверены; анти-петля исчерпывающе безопасна) — в предыдущем комментарии. DROP-лог там же. F1 — реализация выбранного владельцем варианта C; F2/F3 — как раньше. На ре-ревью прогоню тем же прогоном + проверю, что reload больше НЕ привязан к visibility и one-shot держит.
F1: fixed — вариант C реализован. Убрал visibilitychange→reload и немедленный reload скрытой вкладки; visibility больше не влияет на авто-reload. На mismatch: баннер + флаг pendingNavReload; хук useVersionReloadOnNavigation (useLocation + effect по location.key, первый рендер пропущен) на следующей in-app навигации зовёт performAutoReload под тем же shared one-shot. Скрытая вкладка — тем же путём. chunk-boundary не тронут (бэкап для не-навигирующей вкладки). React-router v7 BrowserRouter компонентный — подписка изнутри дерева (UserProvider, где и сокет).
F2: fixed — лог-след перед reload (recordReloadBreadcrumb в sessionStorage переживает reload + console.warn client→server; chunk-boundary помечен своим path; surfacePreviousReloadBreadcrumb на маунте логирует прошлый).
F3: fixed — CHANGELOG + AGENTS.md (version-coherence + артефакт version.json).
Тесты переписаны на MemoryRouter+useNavigate: reload РОВНО раз на первой навигации после mismatch, НЕ на 2-й навигации, НЕ на 2-м mismatch (one-shot), НЕ от ухода в фон; скрытая вкладка — только на навигации; Update — сразу; banner-only/write-fail — никогда. 23 зелёных (шире: features/user 34).
Внутренний цикл (fix-раунд): 1 проход прямым чтением — one-shot гард (markAutoReloaded перед reload) сохранён, visibilitychange полностью убран, первый рендер хука пропущен через useRef.
Ревью #499 — раунд 2 (вариант C). Вердикт: changes-requested (1 мелочь)
Вариант C реализован правильно и — в отличие от соседнего PR — тесты НЕ вхолостую (мутационно доказал, 5 из 6 мутаций краснят suite, включая инвариант защиты от потери данных). Остаётся ровно один блокер: комментарий, который сам этот фикс сделал ложным.
Открыто: F1 (устаревший коммент про авто-reload скрытой вкладки — поведение, которое ты же и убрал).
🔧 Сделать, потом ре-ревью
apps/client/src/features/user/user-provider.tsx:69-70: «the client guard-reloads (banner on a visible tab, auto-reload on a hidden one) before it hits a stale chunk». В r1 это было верно, но r2 (вариант C) УБРАЛ авто-reload скрытой вкладки — именно ради защиты от потери несохранённого ввода. Теперь коммент описывает поведение, которого нет, причём ровно то, что владелец отверг. Cross-file коммент к фиче, которую этот PR меняет, — правь здесь же.Фикс: переписать скобку под вариант C, напр. «(shows a banner and defers the auto-reload to the next in-app navigation)».
✅ Что проверено — вариант C реализован верно, тесты мутационно-надёжны
visibilitychange→reload УБРАН полностью (grep чист). На mismatch: баннер +pendingNavReload=true, БЕЗ reload. Reload только на (a) кнопке Update, или (b) следующей in-app навигации (useVersionReloadOnNavigation:useLocation, первый рендер пропущен, консьюм под one-shot). Скрытая вкладка тоже НЕ перезагружается — ждёт навигации. Вкладку, где юзер не навигейтит, страхует реактивный chunk-boundary. Coherence+stability подтвердили сквозной трассировкой.main.tsx <BrowserRouter>→App <Routes>→Layout→<UserProvider>(единственная точка монтирования хука). Не бросит.performAutoReload(mark перед reload; write-fail→баннер). Проактивный и реактивный делят ОДИН sessionStorage-флаг → максимум один авто-reload/сессия; перм-скью→баннер.recordReloadBreadcrumb(sessionStorage, best-effort try/catch, не бросит) перед reload на ОБОИХ путях (proactive + chunk-boundary помечены своим path);surfacePreviousReloadBreadcrumbна маунте логирует прошлый. F3: CHANGELOG + AGENTS.md — точны, сверено.window.location.reload— реальный spy, ассерты по callCount. Baseline 29 зелёных, server client-version 7/7. Изоляция data-loss покрыта (mismatch без навигации → reload НЕ зовётся).⛔ DROP — кодеру НЕ делать · калибровка
[below-threshold]low/high[test-coverage] ВеткаfirstRender-skip (useVersionReloadOnNavigation, guarded-reload.tsx:158-161) не покрыта — мутация «убрать skip» ВЫЖИЛА (23 зелёных). Но в проде почти недостижима: хук монтируется один раз до того, как WS-mismatch успеет взвестиpendingNavReload(StrictMode-двоймонтаж сбрасывает ref). Оборонительная ветка, near-harmless. Опционально: тест на remount-с-взведённым-флагом → reload не зовётся на первом рендере.Раунд 2, спотлайт: закрытие варианта C (решение владельца) + внесённые фиксом баги + мутационная проверка тестов (урок соседнего PR). Вариант C верен, тесты надёжны, один устаревший коммент — блокер. После правки — approve.
F1: fixed — переписал устаревший коммент в user-provider.tsx:69 под вариант C: «shows a banner and defers the auto-reload to the next in-app navigation (variant C — avoids reloading a backgrounded tab that may hold unsaved input)». Больше не описывает удалённое поведение авто-reload скрытой вкладки. Коммент-only.
Ревью #499 — раунд 3. Вердикт: approved ✅
F1 закрыт: коммент в
user-provider.tsx:69переписан под вариант C — «shows a banner and defers the auto-reload to the next in-app navigation (variant C — avoids reloading a backgrounded tab that may hold unsaved input)». Больше не утверждает удалённое авто-reload скрытой вкладки; теперь точен к shipped-поведению. Изменение comment-only (r2→r3 дельта — только этот файл, 3+/2−), код байт-идентичен r2 (который был зелёным по гейту + мутационно-надёжен по тестам), ре-гейт не нужен.Вариант C (решение владельца) реализован верно, тесты мутационно-надёжны (5/6 мутаций краснят suite, вкл. инвариант защиты данных), useLocation внутри Router, one-shot цел, изоляция data-loss покрыта. Все находки закрыты. → approved, мержить можно.
✅ Staging-приёмка на стенде пройдена (шаги #481) — багов нет
Прогнал приёмку, которую PR помечает «требует staging-проверки вне автостенда»: собрал ДВА образа (
--build-arg APP_VERSION=test-A/test-B), поднял docker-стенд и делал реальный redeploylp-appпод ОТКРЫТОЙ вкладкой (тот жеAPP_SECRET, чтобы cookie/socket.io переподключились). Всё судил out-of-band: WS-кадры socket.io,version.json, boot-лог, консоль.version.json=test-A== шапка (bakedAPP_VERSION)=test-A== WS-кадрapp-version=test-A;/version.jsonотдаётся; boot-логapp-version reload: ACTIVE (v=test-A)test-B; вкладка (test-A) на реконнекте получила WSapp-version=test-B→ показала баннер; видимая вкладка не перезагрузиласьtest-B-бандл, шапка=test-B, версии совпали, баннер ушёл; one-shot флагchunk-reload-attemptedвыставленtest-A(свежий mismatch); только баннер, авто-reload'а нет; клиентская in-app навигация reload не вызвала (sentinel жив, лог фичи молчит; консоль:auto-reload already spent this session — showing manual banner)version.jsonDISABLED (version.json missing/empty), WS-кадр{"version":""}, клиент инертен, ложного баннера нетWS-кадр вживую:
42["app-version",{"version":"test-B"}]на/socket.io/, per-connect (broadcast'а нет — эмит только вhandleConnection).Замечание по методике: variant C (коммит
83f792c) — видимую вкладку не дёргаем; баннер + reload на следующей in-app навигации / клике «Обновить». Тестировал именно это (шапка PR с «hidden→reload» устарела). Первичный «T5-фейл» в сводном прогоне оказался артефактом грубого клика по якорю (полная навигация вместо роутерной) — прицельный тест с sentinel+консолью подтвердил корректное поведение фичи.Вердикт: acceptance #481 закрыт, готов к мержу. Автотесты (server
client-version7/7, client 16/16) + эта staging-проверка сходятся.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.