# Отчёт по тест-стратегии — docmost-sync — 2026-06-16 > Двунаправленная синхронизация статей Docmost с локальным Markdown-git-хранилищем > (git — хранилище состояния). Монорепо: корневое приложение-движок (`src/`) + > библиотека `packages/docmost-client` (~7.5k LOC). Стек: TypeScript ESM, Node ≥ 20, > Vitest 3.2.6. Все тесты лежат в корневом `test/` (`include: ['test/**/*.test.ts']`). ## 1. Исполнительное резюме - **Проанализировано модулей:** 9 (1 субагент `module-testability-analyst` на модуль, все завершились). - **Предложено тестов (unit / integration / contract / E2E):** **50 / 7 / 1 / 2** (итого 60). - unit = 83 % (≥ 70 % ✓), integration = 12 % (≤ 20 % ✓), E2E = 3 % и 2 шт. (≤ 5 % и ≤ 10 ✓). - **Отклонено как малоценные:** ≈ 60 символов/областей (декларативные spec-объекты схемы, тривиальные плоские мапперы, framework-обвязка, type-only интерфейсы, passthrough-обёртки). - **Покрытие сейчас (проверено v8 лично):** **2.6 %** statements по обоим пакетам (искажено огромным непокрытым `docmost-client`). Изолированно: корневое приложение ≈ **40 %**, пакет `docmost-client` ≈ **0 %** (поведенчески покрыт лишь `collectRecentSince`). **Прогноз после Фаз 1–4:** ≈ **60–65 %** (чистые lib-модули 80 %+, корневое приложение ≈ 85 %, транспортный `client.ts` ≈ 40 %). > ⚠️ **Артефакт измерения покрытия.** `package.json` пакета указывает `main: dist/index.js`, > поэтому `import from 'docmost-client'` грузит **скомпилированный `dist/`**, а не `src/`. > v8 меряет `src/` → показывает `client.ts` 0 %, хотя `collectRecentSince` реально исполняется. > **Перед измерением покрытия** добавить в `vitest.config.ts` alias `docmost-client → packages/docmost-client/src/index.ts` > (или мерить по `dist` после сборки), иначе любые новые тесты библиотеки не отразятся в отчёте. > `@vitest/coverage-v8` и скрипт `"coverage"` в проекте отсутствуют — их нужно добавить. ## 2. Рекомендации по модулям ### app-root (`src/`) — движок синка, конфиг, sanitize, round-trip-харнесс - **Извлечь в чистые функции:** `folderSegmentsFor` (`pull.ts:88`, замкнута внутри `main`), `firstDivergence`/`parseArgs` (`roundtrip.ts:101/64`, не экспортированы). - **Unit добавить:** `firstDivergence` (равные/разные деревья, путь расхождения, циклы) — ловит ложное «stable» при реальном расхождении (вся суть харнесса); `nameForNode` (коллизии имён сиблингов → перезапись файлов на диске); `folderSegmentsFor` (вложенность + защита от цикла parent A→B→A, иначе зависание); `parseArgs`; ветка invalid-value в `loadSettingsOrExit` (`config-errors.ts:27-30`, единственный значимый пробел). - **Integration добавить:** `pull.main` с фейковым клиентом + временной директорией (один файл на страницу, верные папки, узлы без id пропускаются) — после R-App-4. - **НЕ тестировать:** `index.ts` (тонкий CLI-passthrough, только `console.log`); `envSchema` (тестировать = тестировать Zod, покрыт через `parseSettings`); тело `roundtrip.main` (байт-стабильность уже покрыта `roundtrip.test.ts`); `invokedDirectly`-guard-блоки; `sanitizeTitle`/`disambiguate`/`parseSettings`/`stripBlockIds` (уже ~100 %). ### client-core (`packages/docmost-client/src/client.ts`, ~2770 строк) — god-object REST+WS клиент - **Извлечь в чистые функции:** валидаторы `isSafeUrl`/`validateDocUrls`/`validateDocStructure` (`client.ts:905/941/1004`), `imageMimeFromPath`/`buildImageNode` (1844/1864) — поднять в `lib/` рядом с `filters.ts`; распаковку конвертов и clamp-логику пагинации (378-393, 1505) в pure-функции. - **Unit добавить:** XSS-allowlist `isSafeUrl`+`validateDocUrls` (`javascript:`/`data:`/`file:`, пробельно-контрольный обход `java\tscript:`, на всех медиа-узлах) — **высший приоритет по безопасности**; `validateDocStructure` (глубина > 200, не-string type); расширить `collectRecentSince` (граница `updatedAt === sinceIso`, элементы без `id`/`updatedAt`); `imageMimeFromPath`+`buildImageNode`; `paginateAll` (стоп-условия, MAX_PAGES=50 + предупреждение, clamp 1..100, оба конверта) — после R-Client-2; `appUrl`/`shareUrl`/`parseCommentContent`; sandbox `transformPage` (`node:vm`: нет `require`/`process`/`fs`, таймаут 5 c, не-функция/не-doc → throw) — security. - **Integration добавить (после R-Client-1, инъекция HTTP):** авто-реавторизация (401-интерсептор + дедуп `login` + `getCollabTokenWithReauth`: один retry, `/auth/login` не ретраится, `loginPromise` сбрасывается в `finally`); `uploadImage` (порядок guard ext→stat→read, > 20 MiB, пересборка FormData на 401, нет утечки тела ответа в ошибку); `createPage` (replay multipart на 401); `checkNewComments` (битая дата → throw, а не «ничего нового»; граница `createdAt > since`; флаг truncated). - **НЕ тестировать:** тонкие REST-passthrough (`getWorkspace`/`getSpaces`/`renamePage`/`movePage`/ `deletePage`/`restorePage`/`listTrash` и пр.) — конверт `data.data ?? data` покрыть один раз извлечённой функцией; делегаты в node-ops/converter/diff (тестировать в их модулях); сами axios/yjs/hocuspocus. ### markdown-conversion (`lib/markdown-converter.ts` + `markdown-document.ts`) — конвертер ProseMirror↔Markdown - **Unit добавить:** табличная golden-матрица по типам узлов (заголовки, маркированные/кодовые спаны, ссылки с title, картинки с пробелами/скобками в src, кодоблоки с языком + срез хвостовых `\n`, GFM-таблицы с выравниванием, spanned-таблицы → `