Тест-стратегия: аудит покрытия и план тестов (10 модулей) #204

Closed
opened 2026-06-26 00:17:57 +03:00 by Ghost · 0 comments

Отчёт по тест-стратегии — gitmost — 2026-06-26

Ветка контекста: develop (анализ выполнен на рабочем дереве feat/198-interrupt-agent-send-now).
Отчёт — это .md-план; исходный/тестовый код не изменялся. Активная фича #198 (прерывание агента / «отправить сейчас») учтена как приоритет фазы 1.

1. Исполнительное резюме

  • Проанализировано модулей: 10 (монорепо pnpm/nx; gitmost — форк Docmost с упором на AI-chat, MCP, редактор).
  • Пакет packages/git-sync исключён из тест-плана: в репозитории отслеживается только build/ (транспилированный JS), исходников нет, сервер его не импортирует — тестируется в своём репозитории.
  • Предложено тестов (unit / integration / contract / E2E): ≈84 / ≈16 / 2 / 0 (всего ≈102). Пирамида соблюдена: unit ≈82 % (≥70 %), integration ≈16 % (≤20 %), E2E = 0 (≤10).
  • Отклонено как малоценное: десятки символов по skip-листу (DTO, DI-обвязка, презентационные компоненты, сторонние библиотеки, тавтологичные снапшоты) — перечислены в разделах «НЕ тестировать» по каждому модулю.
  • Покрытие сейчас: инструментально не измерено — провайдер @vitest/coverage-v8 не установлен, а istanbul-инструментирование Jest падает на ESM-импорте @docmost/editor-ext; и то, и другое требует изменения зависимостей/конфигов (вне мандата «только .md»). Верифицировано фактически — 2312 существующих тестов проходят (server 1336 + client 529 + editor-ext 134 + mcp 313). Качественно: чистая логика и критичные по безопасности пути покрыты хорошо; пробелы — точечные (см. ниже).
  • Прогноз: после внедрения фаз 1–2 закрываются все выявленные дефекты-классы с данными-потерями и регрессиями orchestration; фазы 3–4 закрывают серверные access-control пути (нужен DB-харнесс) и contract-дрейфы.

2. Рекомендации по модулям

server/core/ai-chat (gitmost AI-бэкенд) — покрытие отличное (28 spec)

  • Извлечь в чистые функции: mcp-clients.service.ts:153 (решение release/refcount → decideClose(entry)); экспортировать guardedFetch/lookup (приватны, SSRF-логика недостижима тестом).
  • Unit добавить: жизненный цикл lease/refcount/eviction MCP-клиента (mcp-clients.service.ts:146/220/413) — highest-value gap, ловит утечку/преждевременное закрытие живого клиента и double-close; decryptHeaders (:391, fail-open без auth при ротации APP_SECRET); guardedFetch извлечение host + IP-literal vs hostname (SSRF-обход через форму Request); testServer cleanup в finally (утечка клиента + утечка секрета в текст ошибки).
  • Integration: нет (post-hijack сокет-пламбинг — фреймворк).
  • НЕ тестировать: ai-transcription.service.ts (passthrough), *.module.ts (DI), *.dto.ts, undici/SDK-обвязка — уже покрыто или сторонне.

server/integrations/ai + integrations/mcp — покрытие хорошее, кластер пробелов в STT/settings

  • Unit добавить (топ): transcribeJsonBase64 (ai.service.ts:305) — нормализация URL (двойной слэш), секрет только в заголовке, пустой language не отправляется; embedTexts классификация таймаута (:350) — реальный провайдерский error не маскируется под timeout (дуальное условие, комментарий :370); testConnection (:441) — маппинг ошибок + неутечка ключа; весь AiSettingsService (spec отсутствует): resolve (фоллбэк ключей/URL), update (write-only семантика: undefined=оставить, ''=очистить, значение=записать), getMasked (никогда не отдаёт ключ), resolvePublicShareAssistantName (disabled-персона не утекает); mcpStreamTimeoutMs/mcpCallTimeoutMs env-парсинг.
  • Integration: AiSettingsService.reindex (:83) — порядок dedup BullMQ (remove purge → remove → add со стабильным jobId), .catch не прерывает enqueue.
  • Извлечь: buildJsonTranscriptionRequest(...), общий positiveEnv для embeddingTimeoutMs.
  • НЕ тестировать: контроллеры (guard-обвязка), McpService.handle/hijack (фреймворк, ядро покрыто в mcp.service.spec.ts), *.exception.ts, silentWavProbe.

server/core (Docmost-ядро, без ai-chat) — покрытие ~75–80 %, остаток в recursive-CTE

  • Извлечь: isSharingDisabled(wsSettings, spaceSettings) (share.service.ts:490); applyUserSettingsUpdate(user,dto) (user.service.ts:48); computeNextPosition(last|null) (page.service.ts).
  • Unit добавить: validateAllowedEmail malformed-email guard (auth.util.ts:42) — подтверждённый latent-баг, припаркован как it.todo (1 todo в прогоне); UserService.update смена email (account-takeover без confirmPassword/дубликат); PageService.nextPagePosition (порядок сайдбара); movePage phantom-broadcast guard (numUpdatedRows===0n → нет PAGE_MOVED).
  • Integration (нужен реальный Postgres): getShareForPage наследование share (includeSubPages-гейт, deletedAt) — наивысший blast-radius; isSharingAllowed (toggle disabled → утечка); getShareTree restricted-ancestor; lookupTransclusionForShare access-граф (эксфильтрация); acceptInvitation атомарность + токен-гейт.
  • НЕ тестировать: все *.controller.ts (есть supertest-спеки), favorite/watcher/session/notification/label (тонкая Kysely-делегация), attachment.service S3/queue (механика), tiptap/prosemirror-обёртки.

server/integrations (import/export/storage/crypto/security/env) — покрытие ~40 %, богатый pure-слой

  • Unit добавить (топ): getInternalLinkPageName (export/utils.ts:108) — баг: extensionless путь → пустой текст ссылки; replaceInternalLinks, updateAttachmentUrlsToLocalPaths (битые ссылки/вложения в экспорте); analyzeAttachments (import-attachment.service.ts:672, нужен R1) — Draw.io-пары при Confluence-импорте; notionFormatter ветки callout/bookmark/toggle/inline-math; encodeFilePath/cleanUrlString; resolveTrustProxy (IP-spoofing — сначала проверить, не покрыто ли уже в trust-proxy.util.spec.ts); environment.validation.validate (нужен R2 — снять process.exit).
  • Integration: storageDriverConfigProvider/createStorageDriver (выбор драйвера local/s3/azure, без сети).
  • Извлечь: analyzeAttachments → экспортируемая чистая функция (R1); collectValidationErrors(config) (R2); builder имени/пути вложения из uploadOnce (R3).
  • НЕ тестировать: queue/*, mail/drivers/*, redis, telemetry, audit, S3/Azure SDK-тела, *.module.ts/DTO, статический CSS. Антипаттерн: тавтологичные спеки storage.service.spec.ts:15 и environment.service.spec.ts:14 («should be defined») — заменить на проверки делегирования.

server/common + collaboration + ws — покрытие отличное; один реальный пробел

  • Unit добавить: yjs.util.ts (setYjsMark/removeYjsMarkByAttribute/updateYjsMarkAttribute) — 0 тестов, mark по комментариям в live-collab: off-by-one позиций, утечка mark в codeBlock, порча соседнего commentId (тестируется на in-memory Y.Doc); extractInternalLinkSlugIds (backlink-граф); AuthProvenance-декоратор (фабрика, дрейф с resolveProvenance); withCache try/catch-ветки; InternalLogFilter.log (low prio).
  • НЕ тестировать: html-escaper.ts (сторонний verbatim), interceptors/guards/middlewares (фреймворк), tiptap/hocuspocus-адаптеры, redis-sync обвязка. Известный gap (не тест): ws.service.ts spaceHasRestrictions отдаёт устаревший false до TTL, invalidateSpaceRestrictionCache без продакшен-вызова — нужна integration-проверка, когда приедет эндпоинт-мутатор.

client/features/ai-chat (фронт AI-chat, #198) — pure-слой эталонный; пробел в orchestration

  • Извлечь (R1): decideTurnEnd({isAbort,isDisconnect,isError,intentionalInterrupt}) и решение sendNow из chat-thread.tsx:277/353 (риск: очередь молча теряется на Stop / авто-ретрай после ошибки / «Send now» не флашит); экспортировать rowToUiMessage (:88, дрейф с сервером); инжектировать clock в throttle-эмиттер (:402, R2).
  • Unit: decideTurnEnd/sendNow (после R1); MessageItem пустой reasoning-блок (message-item.tsx:99).
  • Integration (мок @ai-sdk/react): поток «Send now»-прерывания (chat-thread.tsx:353/277) — one-shot interrupted-флаг потребляется ровно один раз; defuse устаревшего флага (:380) — abort не пришёл → не портит будущий тёрн.
  • НЕ тестировать: ConversationList/MessageList scroll (layout в jsdom), презентационные TypingIndicator/ToolCallCard/ChatInput (passthrough), query/service/atom/types. Покрытое (адекватно): весь utils/*, useChatSession (15 кейсов).

client/features/editor (+dictation/page-embed/transclusion) — 152 файла, ценность в pure-utils

  • Unit добавить (топ): normalizeTableColumnWidths (markdown-clipboard.ts:197) — squash столбцов вставленной таблицы; sort-cells.ts (sortItems/weaveItems/isHeaderCell) — порядок/стабильность сортировки, header не уезжает в тело; decideInternalLinkPaste/collectReuploadCandidates (нужен R2) — внутренняя ссылка не превращается в plain link, вложения той же страницы не перезаливаются; ordered-emitter диктовки (use-streaming-dictation.ts:149, нужен R3) — out-of-order seq, stale-epoch, счётчик in-flight; sortFrequentlyUsedEmoji + баг: незащищённый JSON.parse localStorage (emoji-menu/utils.ts:35/64); useLinkEditorState (классификация URL/поиск); transclusion-lookup-context (0 тестов, ручной дубликат page-embed-lookup-context); partial-response ветка page-embed-lookup-context.tsx:83.
  • Integration: handleFileDrop (drag-reorder ≠ file-drop).
  • Извлечь/рефакторить: R1 экспорт parsePixelWidth/deriveColumnWidths; R2 paste-хелперы; R3 ordered-emitter из god-хука (474 LOC).
  • НЕ тестировать: все *-view.tsx node-views, plugin-регистрация, DOM-зависимый table/dnd/calc-drag-over. Покрытое: html-embed-sandbox, decide-embed-state, slash-menu gating/suggestions, subpages-utils, encode-wav.

client/features (auth/comment/share/space/page/...) — покрытие ~80 %, ценность в page-tree

  • Unit добавить: applyUpdateOne (tree-socket-reducers.ts:20 — единственная непокрытая ветка сокет-редьюсера); appendNodeChildren/deleteTreeNode/findBreadcrumbPath/updateTreeNodeName/updateTreeNodeIcon (page/tree/utils/utils.ts — потеря lazy-loaded поддерева, over-deletion); formatRelativeTime (notification.utils.ts:4, с vi.setSystemTime); sortPositionKeys (стабильность компаратора).
  • Извлечь (R1/R2): resolveShareState({share,workspace,space}) из share-modal.tsx:45 (приоритет workspace>space disable, inherited vs own); экспорт zod-схем auth-форм (login-form.tsx:25 и 4 близнеца) для unit-валидации.
  • Антипаттерн: findBreadcrumbPath мутирует вход (utils.ts:53, node.name="Untitled") — зафиксировать тестом и завести issue.
  • НЕ тестировать: все queries/*, services/*, atoms/* (обвязка), use-space-ability (@casl-обёртка), презентационные компоненты.

packages/mcp — покрытие высокое (313 тестов); пробелы в markdown-экспорте медиа

  • Unit добавить: htmlEmbed → markdown (data-loss)markdown-converter.ts default-ветка (~601): атом без case сериализуется в "" и блок исчезает из экспорта (зафиксировать намеренный плейсхолдер ИЛИ round-trip); round-trip video/youtube/embed/excalidraw/audio/pdf (schema.test.mjs проверяет только Yjs-путь, не markdown); footnoteMarkers legacy [N] + notesHeading split (diff.ts); performLogin коллизия имени cookie (auth-utils.ts:59, authTokenRefresh перед authToken); applyAnchorInDoc first-match при двух одинаковых блоках.
  • Contract: паритет SHARED_TOOL_SPECS с реальными потребителями (in-app ai-chat + MCP-регистрация) — текущий tool-specs.test.mjs самореферентен и не ловит дрейф, ради которого создан.
  • НЕ тестировать: stdio.ts/index.ts (обвязка), filters/page-lock/tree/parse-node-arg/text-normalize (покрыто), live image-serving (в test-e2e.mjs). Антипаттерн: schema.test.mjs:75 маскирует покрытие («survives Yjs» ≠ markdown-путь).

packages/editor-ext — покрытие pure-surface ~35 %; высокий blast-radius

  • Unit добавить (топ): transpose (инверсия индексов на не-квадратных таблицах); moveRowInArrayOfRows (off-by-one на чётных блоках — самая ветвистая непокрытая функция); convertTableNodeToArrayOfRows/convertArrayOfRowsToTableNode (round-trip merged-cell → null-плейсхолдер); getBasename; токенайзеры callout.marked/math-block.marked/math-inline.marked (false-positive math из цен $5 and $6); ordered-list start в marked.utils/turndown.utils (нумерация сбрасывается на 1 при экспорте); video-rule turndown (escape имени файла); getReplaceStep overlap-ветка; simplifyTransform.
  • Integration (реальная схема, без DOM): recreateTransformинвариант: применённый diff восстанавливает целевой doc (иначе data-loss в page-history и mcp git-sync); moveRow/moveColumn; getSelectionRangeInColumn (merged-cell off-by-one); addUniqueIdsToDoc (дубликаты/пропуски block-id ломают адресацию MCP).
  • Contract: turndown footnoteDefinition newline-collapse + fillEmptyFootnoteRefs.
  • НЕ тестировать: table/dnd/* (DOM-layout), plugin-обвязка footnote-numbering/sync, node/extension-определения, барреля. Антипаттерн: marked.utils.ts:11 мутирует глобальный singleton marked на импорте — тестировать только через markdownToHtml.

3. Сквозные аспекты

  • Contract-тесты: (1) паритет SHARED_TOOL_SPECS ↔ in-app/MCP-потребители; (2) дрейф recreate-transformvendored-копия editor-ext vs npm-пакет в mcp/diff.ts (один алгоритм, две копии); (3) уже есть ai-agent-role-form.drivers.test.ts и mcp-login-gate-coupling.contract.spec.ts — образец для (1)/(2).
  • Property-based: таблицы (transpose∘transpose=id, round-trip matrix↔node), markdown↔prosemirror round-trip, recreateTransform (apply(diff)=target) — естественные кандидаты после извлечения чистых функций.
  • Test-data factories: PM-узлы через getSchema(extensions)+PMNode.fromJSON (паттерн footnote.test.ts); фабрики строк chat-сообщений (drift сервер↔клиент rowToUiMessage); фикстуры share-графа для DB-интеграций.
  • Дублируемая логика → синхронизировать тестом: transclusion-lookup-contextpage-embed-lookup-context (Gitea #94).

4. Обнаруженные антипаттерны

  • God-методы/хуки: AiChatService.stream (~550 строк, ai-chat.service.ts:258, + временный «Safari»-логгинг к удалению); use-streaming-dictation.ts (474 LOC); orchestration через 5 ref-ов в chat-thread.tsx (проверяемо только чтением комментариев).
  • God-объект на подходе: ShareService (581 LOC, 7 зависимостей) — спеки 3× используют jest.spyOn(service, 'getShareForPage') (запах: recursive-CTE не изолированы) → извлечь ShareGraphRepo.
  • Тавтологичные/слабые тесты: storage.service.spec.ts, environment.service.spec.ts («should be defined»); tool-specs.test.mjs (самореферентный); schema.test.mjs (Yjs вместо markdown).
  • Скрытые сайд-эффекты: findBreadcrumbPath и buildTreeWithChildren мутируют вход (client tree-utils); marked.use(...) на импорте (editor-ext); незащищённый JSON.parse в emoji-menu.
  • Order-dependent/flaky: не найдено в существующих спеках; единственная тайминг-чувствительная точка — ai-streaming-fetch.spec.ts (реальный loopback-сервер, sub-second margins) — наблюдать в CI.

5. Необходимые рефакторинги перед написанием тестов

  • R-ai-chat: экспорт guardedFetch/lookup, извлечение decideClose — блокирует SSRF- и lease-тесты.
  • R-core: computeNextPosition + ShareGraphRepo; DB-харнесс (Postgres) — блокирует 5 share/invitation integration-тестов (recursive-CTE/транзакции).
  • R-integrations: analyzeAttachments→pure (R1); collectValidationErrors без process.exit (R2); builder имени вложения (R3).
  • R-client-ai-chat: decideTurnEnd/sendNow extract (R1), inject clock (R2), export rowToUiMessage (R3).
  • R-client-editor: export parsePixelWidth/deriveColumnWidths (R1), paste-хелперы (R2), ordered-emitter (R3).
  • R-client-core: resolveShareState (R1), экспорт zod-схем auth (R2).
  • editor-ext/mcp: рефакторинг не требуется — всё достижимо через публичные экспорты.

6. План внедрения (по фазам)

  • Фаза 1 — едет вместе с #198 (ROI: активная фича, наибольший непокрытый orchestration-риск): извлечь+покрыть decideTurnEnd/sendNow (chat-thread.tsx), integration «Send now»-прерывания и defuse флага; экспорт+тест SSRF guardedFetch; жизненный цикл lease/refcount MCP-клиента. Закрывает потерю очереди, утечку клиента и SSRF-обход.
  • Фаза 2 — data-integrity чистых функций (ROI: широкий blast-radius, нулевая инфра): editor-ext (table-utils, recreateTransform-инвариант, markdown-токенайзеры); mcp (htmlEmbed + media round-trip, footnote-diff, cookie-парсинг); client (sort-cells, normalizeTableColumnWidths, ordered-emitter диктовки, баг emoji JSON.parse); server import/export (getInternalLinkPageName и др. баги). Фиксирует подтверждённые баги и потери данных.
  • Фаза 3 — серверная безопасность/доступ (ROI: критичный blast-radius, требует DB-харнесс + R-core): share-наследование/isSharingAllowed/transclusion-access/acceptInvitation-атомарность; AiSettingsService (секреты write-only, неутечка ключей); environment.validation (после R2). Параллельно — unit-пробелы ai-chat/ai-integration/common (yjs.util).
  • Фаза 4 — добивка и контракты (ROI: антирегресс/антидрейф): client tree-reducers/utils (applyUpdateOne, tree-utils, formatRelativeTime), transclusion-lookup-context; contract-тесты SHARED_TOOL_SPECS и recreate-transform-паритет; замена тавтологичных спеков на проверки делегирования; завести issue на findBreadcrumbPath-мутацию.

7. Источники

  • Отчёты 10 аналитиков (module-testability-analyst), по одному на модуль; git-sync исключён (только build/).
  • Фактический прогон тестов (верификация «уже покрыто»): server Jest 134 suites / 1336 pass + 1 todo; client Vitest 51 файлов / 529 pass; editor-ext Vitest 14 / 134 pass; mcp node:test / 313 pass. Итого 2312 проходящих тестов.
  • Инструментальное покрытие в %: получить не удалось — @vitest/coverage-v8 не установлен; istanbul-инструментирование Jest падает на ESM-импорте @docmost/editor-ext. Оба требуют изменения зависимостей/конфигурации (вне мандата «только .md»); подтверждённый 1 todo совпал с заявленным latent-багом validateAllowedEmail.
  • Фильтрация: шаг 1 (cross-module dedup) — recreate-transform оставлен на нижнем слое в editor-ext, в mcp — только wrapper diffDocs; normalizeTableColumnWidths клиента (clipboard) и сервера (import) — разные функции, обе сохранены. Шаг 2 (skip-лист) — отброшены DTO/DI/презентация/сторонние/тавтологии по всем модулям. Шаг 3 (пирамида) — соблюдена без урезаний. Шаг 4 (refactor-blocking) — 5 integration-тестов core помечены зависимостью от DB-харнесса. Шаг 6 (adversarial) — каждый предложенный тест назван классом дефекта, который провалится при «реалистичной тихой поломке».
# Отчёт по тест-стратегии — gitmost — 2026-06-26 > Ветка контекста: `develop` (анализ выполнен на рабочем дереве `feat/198-interrupt-agent-send-now`). > Отчёт — это `.md`-план; исходный/тестовый код не изменялся. Активная фича `#198` (прерывание агента / «отправить сейчас») учтена как приоритет фазы 1. ## 1. Исполнительное резюме - **Проанализировано модулей: 10** (монорепо pnpm/nx; gitmost — форк Docmost с упором на AI-chat, MCP, редактор). - **Пакет `packages/git-sync` исключён** из тест-плана: в репозитории отслеживается только `build/` (транспилированный JS), исходников нет, сервер его не импортирует — тестируется в своём репозитории. - **Предложено тестов (unit / integration / contract / E2E): ≈84 / ≈16 / 2 / 0** (всего ≈102). Пирамида соблюдена: unit ≈82 % (≥70 %), integration ≈16 % (≤20 %), E2E = 0 (≤10). - **Отклонено как малоценное:** десятки символов по skip-листу (DTO, DI-обвязка, презентационные компоненты, сторонние библиотеки, тавтологичные снапшоты) — перечислены в разделах «НЕ тестировать» по каждому модулю. - **Покрытие сейчас:** инструментально **не измерено** — провайдер `@vitest/coverage-v8` не установлен, а istanbul-инструментирование Jest падает на ESM-импорте `@docmost/editor-ext`; и то, и другое требует изменения зависимостей/конфигов (вне мандата «только `.md`»). Верифицировано фактически — **2312 существующих тестов проходят** (server 1336 + client 529 + editor-ext 134 + mcp 313). Качественно: чистая логика и критичные по безопасности пути покрыты хорошо; пробелы — точечные (см. ниже). - **Прогноз:** после внедрения фаз 1–2 закрываются все выявленные дефекты-классы с данными-потерями и регрессиями orchestration; фазы 3–4 закрывают серверные access-control пути (нужен DB-харнесс) и contract-дрейфы. ## 2. Рекомендации по модулям ### server/core/ai-chat (gitmost AI-бэкенд) — покрытие отличное (28 spec) - **Извлечь в чистые функции:** `mcp-clients.service.ts:153` (решение `release`/refcount → `decideClose(entry)`); экспортировать `guardedFetch`/`lookup` (приватны, SSRF-логика недостижима тестом). - **Unit добавить:** жизненный цикл lease/refcount/eviction MCP-клиента (`mcp-clients.service.ts:146/220/413`) — **highest-value gap**, ловит утечку/преждевременное закрытие живого клиента и double-close; `decryptHeaders` (:391, fail-open без auth при ротации `APP_SECRET`); `guardedFetch` извлечение host + IP-literal vs hostname (SSRF-обход через форму Request); `testServer` cleanup в `finally` (утечка клиента + утечка секрета в текст ошибки). - **Integration:** нет (post-hijack сокет-пламбинг — фреймворк). - **НЕ тестировать:** `ai-transcription.service.ts` (passthrough), `*.module.ts` (DI), `*.dto.ts`, undici/SDK-обвязка — уже покрыто или сторонне. ### server/integrations/ai + integrations/mcp — покрытие хорошее, кластер пробелов в STT/settings - **Unit добавить (топ):** `transcribeJsonBase64` (`ai.service.ts:305`) — нормализация URL (двойной слэш), секрет только в заголовке, пустой `language` не отправляется; `embedTexts` классификация таймаута (:350) — **реальный провайдерский error не маскируется под timeout** (дуальное условие, комментарий :370); `testConnection` (:441) — маппинг ошибок + неутечка ключа; весь `AiSettingsService` (spec отсутствует): `resolve` (фоллбэк ключей/URL), `update` (write-only семантика: undefined=оставить, ''=очистить, значение=записать), `getMasked` (никогда не отдаёт ключ), `resolvePublicShareAssistantName` (disabled-персона не утекает); `mcpStreamTimeoutMs`/`mcpCallTimeoutMs` env-парсинг. - **Integration:** `AiSettingsService.reindex` (:83) — порядок dedup BullMQ (remove purge → remove → add со стабильным jobId), `.catch` не прерывает enqueue. - **Извлечь:** `buildJsonTranscriptionRequest(...)`, общий `positiveEnv` для `embeddingTimeoutMs`. - **НЕ тестировать:** контроллеры (guard-обвязка), `McpService.handle`/hijack (фреймворк, ядро покрыто в `mcp.service.spec.ts`), `*.exception.ts`, `silentWavProbe`. ### server/core (Docmost-ядро, без ai-chat) — покрытие ~75–80 %, остаток в recursive-CTE - **Извлечь:** `isSharingDisabled(wsSettings, spaceSettings)` (`share.service.ts:490`); `applyUserSettingsUpdate(user,dto)` (`user.service.ts:48`); `computeNextPosition(last|null)` (`page.service.ts`). - **Unit добавить:** `validateAllowedEmail` malformed-email guard (`auth.util.ts:42`) — **подтверждённый latent-баг, припаркован как `it.todo` (1 todo в прогоне)**; `UserService.update` смена email (account-takeover без confirmPassword/дубликат); `PageService.nextPagePosition` (порядок сайдбара); `movePage` phantom-broadcast guard (`numUpdatedRows===0n` → нет `PAGE_MOVED`). - **Integration (нужен реальный Postgres):** `getShareForPage` наследование share (includeSubPages-гейт, deletedAt) — **наивысший blast-radius**; `isSharingAllowed` (toggle disabled → утечка); `getShareTree` restricted-ancestor; `lookupTransclusionForShare` access-граф (эксфильтрация); `acceptInvitation` атомарность + токен-гейт. - **НЕ тестировать:** все `*.controller.ts` (есть supertest-спеки), `favorite/watcher/session/notification/label` (тонкая Kysely-делегация), `attachment.service` S3/queue (механика), tiptap/prosemirror-обёртки. ### server/integrations (import/export/storage/crypto/security/env) — покрытие ~40 %, богатый pure-слой - **Unit добавить (топ):** `getInternalLinkPageName` (`export/utils.ts:108`) — **баг: extensionless путь → пустой текст ссылки**; `replaceInternalLinks`, `updateAttachmentUrlsToLocalPaths` (битые ссылки/вложения в экспорте); `analyzeAttachments` (`import-attachment.service.ts:672`, нужен R1) — Draw.io-пары при Confluence-импорте; `notionFormatter` ветки callout/bookmark/toggle/inline-math; `encodeFilePath`/`cleanUrlString`; `resolveTrustProxy` (IP-spoofing — **сначала проверить, не покрыто ли уже** в `trust-proxy.util.spec.ts`); `environment.validation.validate` (нужен R2 — снять `process.exit`). - **Integration:** `storageDriverConfigProvider`/`createStorageDriver` (выбор драйвера local/s3/azure, без сети). - **Извлечь:** `analyzeAttachments` → экспортируемая чистая функция (R1); `collectValidationErrors(config)` (R2); builder имени/пути вложения из `uploadOnce` (R3). - **НЕ тестировать:** `queue/*`, `mail/drivers/*`, `redis`, `telemetry`, `audit`, S3/Azure SDK-тела, `*.module.ts`/DTO, статический CSS. **Антипаттерн:** тавтологичные спеки `storage.service.spec.ts:15` и `environment.service.spec.ts:14` («should be defined») — заменить на проверки делегирования. ### server/common + collaboration + ws — покрытие отличное; один реальный пробел - **Unit добавить:** `yjs.util.ts` (`setYjsMark`/`removeYjsMarkByAttribute`/`updateYjsMarkAttribute`) — **0 тестов**, mark по комментариям в live-collab: off-by-one позиций, утечка mark в codeBlock, порча соседнего commentId (тестируется на in-memory `Y.Doc`); `extractInternalLinkSlugIds` (backlink-граф); `AuthProvenance`-декоратор (фабрика, дрейф с `resolveProvenance`); `withCache` try/catch-ветки; `InternalLogFilter.log` (low prio). - **НЕ тестировать:** `html-escaper.ts` (сторонний verbatim), interceptors/guards/middlewares (фреймворк), tiptap/hocuspocus-адаптеры, redis-sync обвязка. **Известный gap (не тест):** `ws.service.ts spaceHasRestrictions` отдаёт устаревший `false` до TTL, `invalidateSpaceRestrictionCache` без продакшен-вызова — нужна integration-проверка, когда приедет эндпоинт-мутатор. ### client/features/ai-chat (фронт AI-chat, #198) — pure-слой эталонный; пробел в orchestration - **Извлечь (R1):** `decideTurnEnd({isAbort,isDisconnect,isError,intentionalInterrupt})` и решение `sendNow` из `chat-thread.tsx:277/353` (риск: очередь молча теряется на Stop / авто-ретрай после ошибки / «Send now» не флашит); экспортировать `rowToUiMessage` (:88, дрейф с сервером); инжектировать clock в throttle-эмиттер (:402, R2). - **Unit:** `decideTurnEnd`/`sendNow` (после R1); `MessageItem` пустой reasoning-блок (`message-item.tsx:99`). - **Integration (мок `@ai-sdk/react`):** поток «Send now»-прерывания (`chat-thread.tsx:353/277`) — one-shot `interrupted`-флаг потребляется ровно один раз; defuse устаревшего флага (:380) — abort не пришёл → не портит будущий тёрн. - **НЕ тестировать:** `ConversationList`/`MessageList` scroll (layout в jsdom), презентационные `TypingIndicator/ToolCallCard/ChatInput` (passthrough), query/service/atom/types. Покрытое (адекватно): весь `utils/*`, `useChatSession` (15 кейсов). ### client/features/editor (+dictation/page-embed/transclusion) — 152 файла, ценность в pure-utils - **Unit добавить (топ):** `normalizeTableColumnWidths` (`markdown-clipboard.ts:197`) — squash столбцов вставленной таблицы; `sort-cells.ts` (`sortItems`/`weaveItems`/`isHeaderCell`) — порядок/стабильность сортировки, header не уезжает в тело; `decideInternalLinkPaste`/`collectReuploadCandidates` (нужен R2) — внутренняя ссылка не превращается в plain link, вложения той же страницы не перезаливаются; ordered-emitter диктовки (`use-streaming-dictation.ts:149`, нужен R3) — out-of-order seq, stale-epoch, счётчик in-flight; `sortFrequentlyUsedEmoji` + **баг: незащищённый `JSON.parse` localStorage** (`emoji-menu/utils.ts:35/64`); `useLinkEditorState` (классификация URL/поиск); `transclusion-lookup-context` (**0 тестов**, ручной дубликат `page-embed-lookup-context`); partial-response ветка `page-embed-lookup-context.tsx:83`. - **Integration:** `handleFileDrop` (drag-reorder ≠ file-drop). - **Извлечь/рефакторить:** R1 экспорт `parsePixelWidth`/`deriveColumnWidths`; R2 paste-хелперы; R3 ordered-emitter из god-хука (474 LOC). - **НЕ тестировать:** все `*-view.tsx` node-views, plugin-регистрация, DOM-зависимый `table/dnd/calc-drag-over`. Покрытое: html-embed-sandbox, decide-embed-state, slash-menu gating/suggestions, subpages-utils, encode-wav. ### client/features (auth/comment/share/space/page/...) — покрытие ~80 %, ценность в page-tree - **Unit добавить:** `applyUpdateOne` (`tree-socket-reducers.ts:20` — единственная непокрытая ветка сокет-редьюсера); `appendNodeChildren`/`deleteTreeNode`/`findBreadcrumbPath`/`updateTreeNodeName`/`updateTreeNodeIcon` (`page/tree/utils/utils.ts` — потеря lazy-loaded поддерева, over-deletion); `formatRelativeTime` (`notification.utils.ts:4`, с `vi.setSystemTime`); `sortPositionKeys` (стабильность компаратора). - **Извлечь (R1/R2):** `resolveShareState({share,workspace,space})` из `share-modal.tsx:45` (приоритет workspace>space disable, inherited vs own); экспорт zod-схем auth-форм (`login-form.tsx:25` и 4 близнеца) для unit-валидации. - **Антипаттерн:** `findBreadcrumbPath` **мутирует вход** (`utils.ts:53`, `node.name="Untitled"`) — зафиксировать тестом и завести issue. - **НЕ тестировать:** все `queries/*`, `services/*`, `atoms/*` (обвязка), `use-space-ability` (@casl-обёртка), презентационные компоненты. ### packages/mcp — покрытие высокое (313 тестов); пробелы в markdown-экспорте медиа - **Unit добавить:** **htmlEmbed → markdown (data-loss)** — `markdown-converter.ts` default-ветка (~601): атом без case сериализуется в "" и блок исчезает из экспорта (зафиксировать намеренный плейсхолдер ИЛИ round-trip); round-trip `video/youtube/embed/excalidraw/audio/pdf` (`schema.test.mjs` проверяет только Yjs-путь, не markdown); `footnoteMarkers` legacy `[N]` + notesHeading split (`diff.ts`); `performLogin` коллизия имени cookie (`auth-utils.ts:59`, `authTokenRefresh` перед `authToken`); `applyAnchorInDoc` first-match при двух одинаковых блоках. - **Contract:** паритет `SHARED_TOOL_SPECS` с реальными потребителями (in-app ai-chat + MCP-регистрация) — текущий `tool-specs.test.mjs` **самореферентен** и не ловит дрейф, ради которого создан. - **НЕ тестировать:** `stdio.ts`/`index.ts` (обвязка), `filters/page-lock/tree/parse-node-arg/text-normalize` (покрыто), live image-serving (в `test-e2e.mjs`). **Антипаттерн:** `schema.test.mjs:75` маскирует покрытие («survives Yjs» ≠ markdown-путь). ### packages/editor-ext — покрытие pure-surface ~35 %; высокий blast-radius - **Unit добавить (топ):** `transpose` (инверсия индексов на не-квадратных таблицах); `moveRowInArrayOfRows` (off-by-one на чётных блоках — самая ветвистая непокрытая функция); `convertTableNodeToArrayOfRows`/`convertArrayOfRowsToTableNode` (round-trip merged-cell → null-плейсхолдер); `getBasename`; токенайзеры `callout.marked`/`math-block.marked`/`math-inline.marked` (**false-positive math из цен `$5 and $6`**); ordered-list `start` в `marked.utils`/`turndown.utils` (нумерация сбрасывается на 1 при экспорте); video-rule turndown (escape имени файла); `getReplaceStep` overlap-ветка; `simplifyTransform`. - **Integration (реальная схема, без DOM):** `recreateTransform` — **инвариант: применённый diff восстанавливает целевой doc** (иначе data-loss в page-history и mcp git-sync); `moveRow`/`moveColumn`; `getSelectionRangeInColumn` (merged-cell off-by-one); `addUniqueIdsToDoc` (дубликаты/пропуски block-id ломают адресацию MCP). - **Contract:** turndown `footnoteDefinition` newline-collapse + `fillEmptyFootnoteRefs`. - **НЕ тестировать:** `table/dnd/*` (DOM-layout), plugin-обвязка footnote-numbering/sync, node/extension-определения, барреля. **Антипаттерн:** `marked.utils.ts:11` мутирует глобальный singleton `marked` на импорте — тестировать только через `markdownToHtml`. ## 3. Сквозные аспекты - **Contract-тесты:** (1) паритет `SHARED_TOOL_SPECS` ↔ in-app/MCP-потребители; (2) дрейф `recreate-transform` — **vendored-копия `editor-ext` vs npm-пакет в `mcp/diff.ts`** (один алгоритм, две копии); (3) уже есть `ai-agent-role-form.drivers.test.ts` и `mcp-login-gate-coupling.contract.spec.ts` — образец для (1)/(2). - **Property-based:** таблицы (`transpose∘transpose=id`, round-trip matrix↔node), markdown↔prosemirror round-trip, `recreateTransform` (apply(diff)=target) — естественные кандидаты после извлечения чистых функций. - **Test-data factories:** PM-узлы через `getSchema(extensions)+PMNode.fromJSON` (паттерн `footnote.test.ts`); фабрики строк chat-сообщений (drift сервер↔клиент `rowToUiMessage`); фикстуры share-графа для DB-интеграций. - **Дублируемая логика → синхронизировать тестом:** `transclusion-lookup-context` ⇄ `page-embed-lookup-context` (Gitea #94). ## 4. Обнаруженные антипаттерны - **God-методы/хуки:** `AiChatService.stream` (~550 строк, `ai-chat.service.ts:258`, + временный «Safari»-логгинг к удалению); `use-streaming-dictation.ts` (474 LOC); orchestration через 5 ref-ов в `chat-thread.tsx` (проверяемо только чтением комментариев). - **God-объект на подходе:** `ShareService` (581 LOC, 7 зависимостей) — спеки 3× используют `jest.spyOn(service, 'getShareForPage')` (запах: recursive-CTE не изолированы) → извлечь `ShareGraphRepo`. - **Тавтологичные/слабые тесты:** `storage.service.spec.ts`, `environment.service.spec.ts` («should be defined»); `tool-specs.test.mjs` (самореферентный); `schema.test.mjs` (Yjs вместо markdown). - **Скрытые сайд-эффекты:** `findBreadcrumbPath` и `buildTreeWithChildren` мутируют вход (client tree-utils); `marked.use(...)` на импорте (editor-ext); незащищённый `JSON.parse` в emoji-menu. - **Order-dependent/flaky:** не найдено в существующих спеках; единственная тайминг-чувствительная точка — `ai-streaming-fetch.spec.ts` (реальный loopback-сервер, sub-second margins) — наблюдать в CI. ## 5. Необходимые рефакторинги перед написанием тестов - **R-ai-chat:** экспорт `guardedFetch`/`lookup`, извлечение `decideClose` — блокирует SSRF- и lease-тесты. - **R-core:** `computeNextPosition` + `ShareGraphRepo`; **DB-харнесс (Postgres)** — блокирует 5 share/invitation integration-тестов (recursive-CTE/транзакции). - **R-integrations:** `analyzeAttachments`→pure (R1); `collectValidationErrors` без `process.exit` (R2); builder имени вложения (R3). - **R-client-ai-chat:** `decideTurnEnd`/`sendNow` extract (R1), inject clock (R2), export `rowToUiMessage` (R3). - **R-client-editor:** export `parsePixelWidth`/`deriveColumnWidths` (R1), paste-хелперы (R2), ordered-emitter (R3). - **R-client-core:** `resolveShareState` (R1), экспорт zod-схем auth (R2). - **editor-ext/mcp:** рефакторинг не требуется — всё достижимо через публичные экспорты. ## 6. План внедрения (по фазам) - **Фаза 1 — едет вместе с `#198` (ROI: активная фича, наибольший непокрытый orchestration-риск):** извлечь+покрыть `decideTurnEnd`/`sendNow` (`chat-thread.tsx`), integration «Send now»-прерывания и defuse флага; экспорт+тест SSRF `guardedFetch`; жизненный цикл lease/refcount MCP-клиента. Закрывает потерю очереди, утечку клиента и SSRF-обход. - **Фаза 2 — data-integrity чистых функций (ROI: широкий blast-radius, нулевая инфра):** editor-ext (table-utils, `recreateTransform`-инвариант, markdown-токенайзеры); mcp (htmlEmbed + media round-trip, footnote-diff, cookie-парсинг); client (`sort-cells`, `normalizeTableColumnWidths`, ordered-emitter диктовки, баг emoji JSON.parse); server import/export (`getInternalLinkPageName` и др. баги). Фиксирует подтверждённые баги и потери данных. - **Фаза 3 — серверная безопасность/доступ (ROI: критичный blast-radius, требует DB-харнесс + R-core):** share-наследование/`isSharingAllowed`/transclusion-access/`acceptInvitation`-атомарность; `AiSettingsService` (секреты write-only, неутечка ключей); `environment.validation` (после R2). Параллельно — unit-пробелы ai-chat/ai-integration/common (`yjs.util`). - **Фаза 4 — добивка и контракты (ROI: антирегресс/антидрейф):** client tree-reducers/utils (`applyUpdateOne`, tree-utils, `formatRelativeTime`), `transclusion-lookup-context`; contract-тесты `SHARED_TOOL_SPECS` и `recreate-transform`-паритет; замена тавтологичных спеков на проверки делегирования; завести issue на `findBreadcrumbPath`-мутацию. ## 7. Источники - **Отчёты 10 аналитиков** (module-testability-analyst), по одному на модуль; git-sync исключён (только `build/`). - **Фактический прогон тестов (верификация «уже покрыто»):** server Jest 134 suites / **1336 pass + 1 todo**; client Vitest 51 файлов / **529 pass**; editor-ext Vitest 14 / **134 pass**; mcp `node:test` / **313 pass**. Итого **2312 проходящих тестов**. - **Инструментальное покрытие в %:** получить не удалось — `@vitest/coverage-v8` не установлен; istanbul-инструментирование Jest падает на ESM-импорте `@docmost/editor-ext`. Оба требуют изменения зависимостей/конфигурации (вне мандата «только `.md`»); подтверждённый `1 todo` совпал с заявленным latent-багом `validateAllowedEmail`. - **Фильтрация:** шаг 1 (cross-module dedup) — `recreate-transform` оставлен на нижнем слое в `editor-ext`, в `mcp` — только wrapper `diffDocs`; `normalizeTableColumnWidths` клиента (clipboard) и сервера (import) — разные функции, обе сохранены. Шаг 2 (skip-лист) — отброшены DTO/DI/презентация/сторонние/тавтологии по всем модулям. Шаг 3 (пирамида) — соблюдена без урезаний. Шаг 4 (refactor-blocking) — 5 integration-тестов core помечены зависимостью от DB-харнесса. Шаг 6 (adversarial) — каждый предложенный тест назван классом дефекта, который провалится при «реалистичной тихой поломке».
vvzvlad added the test label 2026-06-26 00:32:03 +03:00
Ghost closed this issue 2026-06-28 03:43:29 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#204