diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..95b401d --- /dev/null +++ b/TESTING.md @@ -0,0 +1,255 @@ +# Как тестировать 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 либы +```