# Как тестировать docmost-sync Гайд по проверке проекта — от быстрых офлайн-тестов (без Docmost) до живого прогона против реального инстанса. Дизайн и термины — в [SPEC.md](SPEC.md), конвенции — в [AGENTS.md](AGENTS.md). > **TL;DR.** `make install && npm test` — весь набор без всякой сети. Идемпотентность > конвертера — `npm run roundtrip`. Живой синк: заполнить `.env`, затем `npm run pull` > (read-only из Docmost) и `npm run push` (**dry-run** — план без записи; запись только > с `-- --apply`). --- ## 0. Пререквизиты - **Node ≥ 20** и **npm**. - **Системный `git` на PATH** — это state store (SPEC §5); движок шеллится в `git`. Без него движок честно падает с понятным сообщением (а не сырым стэком). - Установка зависимостей (монорепо npm-workspaces): `make install` (или `npm ci`). ```bash make install # npm ci: ставит корень + packages/docmost-client npm run build # собирает либу, затем движок -> build/ ``` Для **живых** прогонов (pull/push/`--page`) нужен `.env`: ```bash make env # cp .env.example .env (если ещё нет), затем впишите значения ``` Переменные (`.env`, читаются через `src/settings.ts` на zod; отсутствие обязательной — падение на старте с указанием имени): | Переменная | Обяз. | Что это | |---|---|---| | `DOCMOST_API_URL` | да | URL API инстанса, напр. `https://docs.example.com/api` | | `DOCMOST_EMAIL` / `DOCMOST_PASSWORD` | да | креды для `/auth/login` | | `DOCMOST_SPACE_ID` | да | какое пространство зеркалить | | `VAULT_PATH` | нет | git-vault, по умолчанию `data/vault` (gitignored) | | `GIT_REMOTE` | нет | git-remote для пуша vault (пока не используется) | | `POLL_INTERVAL_MS`, `DEBOUNCE_MS`, `LOG_LEVEL` | нет | тюнинг (дефолты разумны) | `.env` и `data/` — в `.gitignore`; **реальные креды не коммитить** (SPEC §12). --- ## 1. Автотесты (офлайн, без Docmost) Весь набор — детерминированный, без сети, без живого инстанса. ```bash npm test # vitest run — ~747 тестов (2 skipped — это live-e2e, см. §4.5) make test # то же через Makefile npm run test:watch # watch-режим npm run coverage # v8-покрытие по src/ и packages/docmost-client/src/ ``` Что покрыто (по слоям): - **Чистая логика (high-ROI):** конвертер ProseMirror↔Markdown (golden + property), каноникализация (`canonicalize`), реконсиляция pull/push (`reconcile`, `compute-pull-actions`, `compute-push-actions`, `classify-rename-moves`), node-ops, transforms, diff, sanitize/layout, settings. - **git-слой (`VaultGit`)** — интеграционные тесты на временных репозиториях под `os.tmpdir()` (init/branches/commit-провенанс/merge/ff/`diffNameStatus`/refs). - **REST-клиент** — через `axios-mock-adapter` (биндинги delete/restore/trash/ move/recent и т.д.), без реальной сети. - **Collab-путь записи** — через настоящий `Y.Doc` (без Hocuspocus-сервера). - **Оркестрация pull/push** — через инъецированные fakes; плюс `run-push-realgit` гоняет `--apply`-путь против НАСТОЯЩЕГО `VaultGit` (ловит регрессии биндинга). Тесты — это и есть «контракт». Падающий `npm test` блокирует docker-сборку (CI). --- ## 2. Идемпотентность round-trip (SPEC §11 / «Задача №0») git диффает побайтово: если `export → import → export` недетерминирован, каждый pull рождает ложный дифф. Harness проверяет это **офлайн**. ```bash npm run build # Один фикстур (по умолчанию test/fixtures/sample-doc.json): node build/roundtrip.js --fixture test/fixtures/sample-doc.json # Весь синтетический корпус (заголовки, марки, списки, таблицы, callout'ы, # код с хвостовым \n, диаграммы, mention/textStyle): node build/roundtrip.js --corpus # default dir test/fixtures/corpus npm run roundtrip -- --corpus # то же через npm-скрипт ``` Вывод: для каждого файла — `md=ok canon=ok`. Гарантии: - **markdown byte-stable** (`md1 === md2`) — свойство, нужное git; - **canonically stable** — семантическое равенство со снятыми block-id и нормализованными дефолтами схемы. **Код выхода:** `0` — всё стабильно (CI-able), `1` — найдено расхождение (печатает первую дивергенцию). Известные ограничения конвертера зафиксированы честно через `it.fails` (блочная картинка между блоками; `code`-марка с другой маркой). Живой вариант (реальная страница, нужен `.env`): ```bash node build/roundtrip.js --page ``` --- ## 3. Живой pull — Docmost → vault (read-only к Docmost) `pull` зеркалит сконфигурированное пространство в локальный git-vault. **К Docmost он строго read-only** (только читает) — безопасно гонять против боевого инстанса. ```bash # заполните .env, затем: npm run pull # = node build/pull.js make pull ``` Что происходит (SPEC §6, Docmost→ФС): 1. `ensureRepo` создаёт vault-репо (`data/vault`) с ветками `main` и `docmost`. 2. Обход дерева пространства (`/pages/sidebar-pages`) + выгрузка тел (без комментариев, только якоря, §3) → запись `.md` в дерево папок `Space/…/Title.md`. 3. Коммит на ветке `docmost` → merge в `main`. Проверить результат: ```bash ls -R data/vault # дерево статей в Markdown git -C data/vault log --oneline --all # коммиты docmost:/local: git -C data/vault branch # main + docmost git -C data/vault show refs/docmost/last-pushed # (появится после push) cat data/vault//<…>/.md # meta-блок + тело + якоря ``` Защиты: при **неполном** обходе дерева удаления подавляются (§8), есть порог на **массовое** удаление, и стартовый guard на незавершённый merge (§9/§12). --- ## 4. Живой push — vault → Docmost (запись!) `push` транслирует локальные правки `.md` обратно в Docmost. Это **единственный путь записи в Docmost**, поэтому он **dry-run по умолчанию**. ### 4.1 Dry-run (безопасно — план без записи) ```bash npm run push # = node build/push.js — DRY-RUN: только печатает план ``` Dry-run **ничего не пишет в Docmost** и не двигает рефы. Он печатает план: что будет create / update / delete / move / rename / noop / skipped. Важно: чтобы посчитать дифф `base..main`, dry-run **локально коммитит** ожидающие правки рабочего дерева на ветке `main` (это локальный git, в Docmost ничего не уходит — об этом есть строка в логе). Типичный сценарий проверки: ```bash npm run pull # 1. зеркалим пространство в vault # 2. правим/добавляем/переименовываем/удаляем .md в data/vault руками npm run push # 3. смотрим план (dry-run) — что уедет в Docmost ``` ### 4.2 Apply (реальная запись в Docmost) ```bash npm run push -- --apply # ВЫПОЛНЯЕТ план: пишет в Docmost ``` Что делает (SPEC §6, ФС→Docmost): - **modified** → `import_page_markdown` через **collab/Yjs-путь** (не сырой jsonb-overwrite, §2); - **added без pageId** → `create_page`, присвоенный `pageId` пишется обратно в meta; - **deleted** → `delete_page` — это **soft-delete** в Trash Docmost (**обратимо**, §8); - **renamed/moved** → `move_page` / `rename_page` (истина положения — путь файла, §5; чистое локальное переименование без смены родителя/title — **noop**, в Docmost не транслируется). После чистого применения двигаются `refs/docmost/last-pushed` и ветка `docmost` (loop-close, §6.3/§10) — чтобы следующий pull не утянул свою же запись обратно. ### 4.3 Безопасность записи - **Начинайте с dry-run** и читайте план. - Тестируйте на **отдельном/тестовом пространстве**, а не на боевом. - Удаления **обратимы**: уходят в Docmost Trash (восстановление — Trash в UI; авто- чистка ~30 дней). Локально всё восстановимо из git-истории vault. - Конфликт-маркеры **никогда** не уезжают в Docmost (§9): при незавершённом merge push прерывается с понятным сообщением. - Рекомендуется **один авторитетный прогон/демон** на пространство (не пушить вдвоём). ### 4.4 Проверить, что dry-run действительно ничего не пишет ```bash node build/push.js # без --apply: клиент Docmost даже не строится ``` (Это же гарантирует автотест: dry-run делает ноль вызовов клиента и ноль движений рефов; путь записи достижим только с `--apply` + реальными кредами.) ### 4.5 Живой e2e-smoke `test/e2e-docmost.test.ts` — 2 теста, **пропущены** без реальных кред (поэтому в `npm test` всегда «2 skipped»). Это сквозной дым против живого инстанса; включается при наличии `.env` (см. начало файла теста). --- ## 5. Что уже реализовано, а что — нет Чтобы тестировать в границах реального: **Готово и проверяемо:** - pull (Docmost→vault) — цикл §6 с реконсиляцией и guard'ами безопасности; - push (vault→Docmost) — полное покрытие типов изменений (create/update/delete/ move/rename/noop), runnable `npm run push` (dry-run / `--apply`), loop-close; - идемпотентный round-trip (§11), git-слой, REST-биндинги, collab-запись. **Ещё НЕ реализовано (ручной прогон команд, не демон):** - непрерывный **FS-watcher + дебаунс** (§7.1) — сейчас push запускается вручную; - **поллинг-цикл** pull — сейчас pull одноразовый; - **`git push` в remote** (§7.2) — vault пока локальный; - точная **fractional-index позиция** при move (пока серверный дефолт); - потребление loop-guard-записи на pull-стороне (git-native ff-`docmost` уже закрывает контентную петлю). Иными словами: обе **стороны** синка работают как **одноразовые команды**; непрерывного демона ещё нет. --- ## 6. Траблшутинг | Симптом | Причина / что делать | |---|---| | `git binary not found …` | поставьте системный `git` (нужен для vault). | | `Configuration error … Missing required variable(s)` | заполните `.env` (см. §0). | | `vault has an unresolved merge …` | в vault остался конфликт: `git -C data/vault merge --abort` (или разрешите), затем повторите (§9/§12). | | `pull: tree fetch incomplete — deletions suppressed` | часть дерева не подтянулась; удаления намеренно не применены — повторите pull (§8). | | push «всё-или-ничего» при сбое страницы | при ошибке любой страницы рефы НЕ двигаются → повторный прогон чистый (§12). | | `WARNING — the 'docmost' mirror branch DIVERGED` | кто-то писал в ветку `docmost` руками (нарушение §5) — она пишется только движком. | | round-trip exit 1 | конвертер недетерминирован на этом контенте — см. вывод первой дивергенции (§11). | Полезное: ```bash npm run build && npm test # быстрый sanity всего node build/roundtrip.js --corpus # идемпотентность git -C data/vault log --oneline --all --decorate make clean # снести build/ + node_modules + dist либы ```