[bug][ui] Автотест стенда (проход #2): share-not-bound-to-shareId, new-page-empty-body, editor-read-only-window, breadcrumb-lag, page-info-leak, callout-paste #218
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Второй проход автономного тестирования (web-test-orchestrator, прогон против ПЕРЕСОБРАННОГО feat/git-sync с фиксами data-loss). Только гитмост-UI баги. git-sync дублирование — починено в PR #119 (см. отдельный отчёт-ишью). Дубли прошлого прохода (#216) и уже-исправленное помечены.
[medium] Доступ к содержимому публичной share-страницы не привязан к shareId — поддельный/невалидный shareId всё равно отрисовывает полную share-страницу
Воспроизведение: В свежем инкогнито-контексте откройте http://localhost:5173/share/doesnotexist99/p/topcause-4242-hA5kn4vwqQ (настоящий slug расшаренной страницы, выдуманный shareId). Страница отрисовывается полностью. Перекрёстная проверка: POST /api/shares/tree {"shareId":"doesnotexist99"} -> 404 'Share not found', но POST /api/shares/page-info {"pageId":"hA5kn4vwqQ"} (без shareId) -> 200 с полным содержимым.
Доказательство: Verifier воспроизвёл: tree API отдаёт 404 для поддельного shareId, тогда как page-info отвечает 200 и возвращает страницу целиком (тело TOPCAUSE4242). Первопричина в apps/server/src/core/share/share.service.ts getSharedPage() (~L191-211) вызывает resolveReadableSharePage(null, dto.pageId, ...) — shareId намеренно передаётся null, поэтому доступ к содержимому ограничен лишь случайным 10-символьным slugId страницы и проверкой «расшарена ли вообще эта страница». Клиент apps/client/src/pages/share/shared-page.tsx (L25-27) запрашивает page-info только по slugId. Более того, useEffect (L32-45) редиректит несовпадающий shareId на канонический URL, ТЕМ САМЫМ УТЕКАЯ настоящий share-ключ тому, кто угадал только slug. Per-link секрет, показываемый в диалоге Share, не является границей доступа и не может быть ротирован/отозван без снятия шаринга. Эксплуатируемость ограничена (slugId — случайный nanoid, который атакующий должен знать); вероятно, унаследованное поведение upstream Docmost.
[medium] Новая страница молча сохраняет пустое тело, если контент набран до подключения Yjs/collab-провайдера (потеря данных)
Воспроизведение: Создайте страницу, сразу начните набирать тело (до подключения collab-websocket), затем перезагрузите или уйдите со страницы. Тело сохраняется пустым.
Доказательство: Verifier подтвердил через git-export gold standard на свежем клоне: 'MEARLY 72474.md' содержит только frontmatter, хотя заголовок сохранился; тела FASTMARK/SLOWMARK отсутствуют во всех 108+ md-файлах; контрольные образцы с 7s паузой перед набором сохранились полностью (VCTRL, VPASTE). В момент захвата потерянные тела БЫЛИ в живом ProseMirror DOM. Реальный триггер скорректирован относительно гипотезы репортёра: это НЕ синтетическая вставка (VPASTE сохранился) и НЕ навигация (MEARLY потерял тело без навигации + 12s паузы) — это набор текста в только что созданную collab-страницу до подключения Yjs-провайдера (ws://...:3001/collab), так что нажатия клавиш попадают только в локальный ProseMirror и никогда не доходят до сохраняемого shared doc. Ноль ошибок в консоли. Требует редактирования только что созданной страницы в пределах sub-7s окна подключения; более медленное редактирование существующих страниц безопасно.
[low] Редактор read-only (contenteditable=false) ~5s после открытия; правки, набранные в этом окне, молча отбрасываются
Воспроизведение: Откройте страницу через /p/ и сразу (до подключения collab-websocket) кликните по абзацу и наберите текст.
Доказательство: Verifier подтвердил в исходниках apps/client/src/features/editor/page-editor.tsx: showStatic=useState(true) (L458) переключается в false только когда Yjs Connected + isLocalSynced + isRemoteSynced (L460-469); во время showStatic рендерится EditorProvider с editable=false, показывающий кэшированный контент (L475-486), а редактируемый редактор монтируется только после синхронизации (L487-490). Набор текста в статической фазе попадает в editable=false редактор и затем заменяется синхронизированным doc. Молчаливый аспект подтверждён: page-header-menu.tsx (L383-428) показывает лишь иконку wifi-off 'connection lost' после 5000ms дисконнекта — никакого read-only/loading-индикатора в первые секунды. Механизм подтверждён кодом; живой образец contenteditable=false не захвачен, потому что медленный dev-mount происходит уже после синхронизации (оговорка инструментария).
[low] Breadcrumb на глубоких страницах часто пустой несколько секунд (отстаёт от основного контента)
Воспроизведение: Откройте страницу 3-го уровня вложенности напрямую по URL (например, TN Linger > TN Child B > TN Mover). Следите за breadcrumb в шапке страницы; перезагрузите несколько раз.
Доказательство: Verifier воспроизвёл: в окне 8s путь из >=2 сегментов появился лишь на 1/5 загрузок (5705ms); в окне 25s он появился на 9204ms/12735ms, т.е. на 2358ms и 8539ms ПОЗЖЕ, чем отрендерился H1 редактора — то есть breadcrumb специфически отстаёт от контента. НОЛЬ ошибок в консоли и НОЛЬ HTTP 4xx/5xx за период пустоты; когда заполняется, путь корректен с настоящими -ссылками. Первопричина в apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx (L43-48): useEffect защищён условием treeData?.length > 0 && currentPage и вызывает findBreadcrumbPath(treeData, currentPage.id); treeData — это atom дерева сайдбара, заполняемый лениво/независимо от page-запроса, поэтому пока дерево не содержит цепочку предков,
рендерится пустым. Vite-dev миллисекунды раздуты, но устойчивое относительное отставание (breadcrumb позади H1) реально. Низкая важность: всегда в итоге рендерится корректно.[low] Публичный page-info endpoint раскрывает внутренние метаданные (user/space/workspace ID) анонимным зрителям
Воспроизведение: Без аутентификации: curl -X POST http://localhost:3000/api/shares/page-info -H 'Content-Type: application/json' -d '{"pageId":"hA5kn4vwqQ"}'
Доказательство: Verifier воспроизвёл HTTP 200, чей объект data.page раскрывает creatorId, lastUpdatedById, contributorIds[], spaceId, workspaceId, lastUpdatedAiChatId, lastUpdatedSource, isLocked, isTemplate, parentPageId, position, createdAt/updatedAt/deletedAt; data.share дополнительно повторно утекает creatorId/spaceId/workspaceId. Это действительно публичный share-путь (валидный share-ключ присутствует), а не обход авторизации. Влияние незначительное — все непрозрачные UUID, без PII/секретов — но больше, чем нужно публичному рендереру.
[low] Вставка markdown-синтаксиса callout
> [!info]в редактор даёт обычный blockquote с буквальным текстом '[!info]'Воспроизведение: В теле UI-страницы вставьте plain-text markdown, содержащий
> [!info]\n> body(например, через буфер обмена).Доказательство: Verifier воспроизвёл: вставленный блок отрендерился как
(количество callout-нод 0), тогда как heading/вложенный список/code-fence в той же вставке сконвертировались корректно. Подтверждено юнит-тестом: markdownToHtml('> [!info]...') => blockquote с буквальным [!info]; markdownToHtml(':::info...') => корректный callout div. Первопричина в packages/editor-ext/src/lib/markdown/utils/callout.marked.ts: правило токенизатора /^:::([a-zA-Z0-9]+)\s+([\s\S]+?):::/ распознаёт только форму:::type ... :::, но не GitHub> [!type]. Путь вставки (apps/client/src/features/editor/extensions/markdown-clipboard.ts handlePaste) проходит через тот же markdownToHtml. ПРИМЕЧАНИЕ: касается только in-editor-вставки — путь git-ИМПОРТА конвертирует> [!type]корректно (отдельный code path, проверено положительно), так что это побочно к git-sync. Без краша/потери данных; остаётся редактируемым blockquote.Ghost referenced this issue2026-06-27 18:48:39 +03:00
Ghost referenced this issue2026-06-27 21:03:47 +03:00
Ghost referenced this issue2026-06-27 21:32:55 +03:00
Ghost referenced this issue2026-06-27 21:55:17 +03:00
Ghost referenced this issue2026-06-27 22:12:37 +03:00