docs: add TESTING.md (how to test docmost-sync)
Some checks failed
Test and publish image / test (push) Has been cancelled
Test and publish image / build (push) Has been cancelled

Root-level testing guide: prerequisites (Node 20+, system git, .env), the offline
test suite + coverage, the §11 round-trip idempotency harness (--fixture/--corpus/
--page), and live testing against a real Docmost — read-only pull, dry-run push
(plan only) vs --apply (writes), vault git inspection, what's implemented vs not,
safety/recovery (Trash is reversible, conflicts never reach Docmost), and a
troubleshooting table.
This commit is contained in:
vvzvlad
2026-06-21 02:31:51 +03:00
parent 5826255a2f
commit b03eb353c1

255
TESTING.md Normal file
View File

@@ -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 <pageId>
```
---
## 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/<Space>/<…>/<Title>.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 либы
```