docs: add TESTING.md (how to test docmost-sync)
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:
255
TESTING.md
Normal file
255
TESTING.md
Normal 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 либы
|
||||
```
|
||||
Reference in New Issue
Block a user