test(server): integration harness + deferred coverage vs real Postgres/Redis

Builds the deferred integration tests from docs/backlog/feature-test-coverage-
deferred.md that needed real infra (a test Postgres + real Redis) which the repo
lacked. Runs against an isolated, auto-created docmost_test database and Redis
logical DB 15 — never the dev data.

Harness (apps/server/test/integration/, run via new `pnpm --filter server test:int`
=> jest --config test/jest-integration.json; default unit `jest` is untouched and
excludes these via the *.int-spec.ts name + rootDir):
- db.ts: buildTestDb() mirrors database.module.ts exactly (PostgresJSDialect,
  CamelCasePlugin, bigint to:20/from:[20,1700] parsing) + minimal seed helpers.
- global-setup.ts: DROP/CREATE docmost_test, CREATE EXTENSION vector, migrate to
  latest via Kysely Migrator (fails loud on any errored migration).
- global-teardown.ts: closes the pool.

Coverage (5 suites, 16 tests, all green against live PG+Redis):
- WorkspaceRepo.updateSetting: jsonb-merge persists htmlEmbed without clobbering
  sibling ai/sharing namespaces (the kill-switch write half).
- AiAgentRoleRepo: soft-delete exclusion, cross-workspace tenant isolation,
  duplicate (name,workspace) -> 23505, name reusable after softDelete (partial
  unique index WHERE deleted_at IS NULL), same name across workspaces allowed.
- page_template_references: deleting either source or referenced page cascades
  the link row (onDelete cascade) — real FK, not mocked.
- PublicShareWorkspaceLimiter vs REAL Redis: real ioredis EVAL of the sliding-
  window Lua — max boundary (3 admit / 4th deny), re-admit after the window
  slides, same-ms distinct members. Catches Lua bugs a FakeRedis cannot.
- AiChatRepo.findByCreator: role-badge join (enabled->badge; soft-deleted or
  disabled role -> null).

Review: APPROVE; applied its two hardening suggestions (fail loud on errored
migration result even without a top-level error; TEST_REDIS_URL override + ping
preflight). tsc clean; unit run excludes int-spec (verified).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 07:02:55 +03:00
parent e5bc82c7f1
commit 04f05626ad
11 changed files with 685 additions and 93 deletions

View File

@@ -1,93 +0,0 @@
# Отложенные тесты по фичам с коммита 053a9c0d (хвост от PR #49)
## Контекст
PR #49 («test: cover features since 053a9c0d + repair test tooling») закрыл
основную массу покрытия новых фич gitmost (+~330 тестов: server/Jest,
client/Vitest, editor-ext/Vitest, packages/mcp/node:test) и починил
тест-инструментарий (FIX-0 сломанные спеки transclusion, BUILD-0 сборка
editor-ext перед серверными тестами, INFRA-0 резолв `.tsx` email-шаблонов).
Часть тестов из принятого тест-плана **намеренно отложена** — им нужен
тестовый Postgres, реальный Redis или HTTP/e2e-харнес, которых в проекте
сейчас нет, либо инвазивный рефактор продакшн-кода. Ниже — что осталось и
почему, чтобы не потерять.
---
## 1. Интеграционные тесты против БД (нужен тестовый Postgres)
Сейчас все repo-зависимые проверки делаются на моках; SQL-уровень не
исполняется. Чтобы покрыть это честно, нужен поднимаемый в CI Postgres
(testcontainers или сервис в pipeline) + хелпер миграций.
- **`AiAgentRoleRepo` — изоляция и индексы.**
`apps/server/src/database/repos/ai-agent-roles/ai-agent-roles.repo.ts`.
Проверить против реальной БД: `findById`/`listByWorkspace` исключают
soft-deleted строки; `findById` для roleId из ЧУЖОГО workspace → undefined
(tenant-изоляция); дубль имени в одном workspace → 23505; то же имя
переиспользуемо после softDelete (partial unique index
`WHERE deleted_at IS NULL`, миграция `20260620T120000-ai-agent-roles.ts`);
одинаковое имя в разных workspace разрешено. Это «хребет» безопасности —
сейчас только предполагается unit-моками.
- **`AiChatRepo.findByCreator` — join role-badge.**
`apps/server/src/database/repos/ai-chat/ai-chat.repo.ts` (~:27-70).
Чат с enabled-ролью → roleName/roleEmoji заполнены; с soft-deleted ролью →
бейдж NULL; с DISABLED ролью → бейдж NULL (должно совпадать с
`resolveRoleForRequest`); ORDER BY квалифицирован `aiChats.*` (нет
ambiguous column после join). Не проверяемо чистым unit-ом.
- **`WorkspaceService.update` / `WorkspaceRepo.updateSetting` — jsonb-merge.**
`apps/server/src/core/workspace/services/workspace.service.ts` (~:514),
`apps/server/src/database/repos/workspace/workspace.repo.ts` (~:275).
Сейчас покрыта только форма вызова сервиса
(`workspace-html-embed.spec.ts`). Не покрыто (нужна БД): `htmlEmbed:true`
персистится через jsonb-merge **не затирая** соседние настройки (ai,
sharing). Это и есть «kill-switch пишется» — критично, что write-половина
тоггла не ломает остальной settings-namespace.
- **FK `page_template_references` onDelete('cascade').**
Миграция `20260620T131000-page-template-references.ts`. Проверить, что
удаление source/reference-страницы каскадит строки ссылок.
## 2. HTTP / e2e-харнес (его нет в apps/server)
- **Public-share ассистент: обход per-IP throttle ротацией XFF, но
per-workspace cap держит.**
Контроллер использует стоковый `@UseGuards(ThrottlerGuard)`
(`apps/server/src/core/ai-chat/public-share-chat.controller.ts`), IP берётся
из Fastify `trustProxy``X-Forwarded-For`. Единственный оправданный e2e
(named journey «аноним спамит ассистента»): ротация XFF обходит per-IP
лимит 5/min, но per-workspace cost-cap всё равно отдаёт 429. Требует
поднятого HTTP-слоя Nest + trusted-proxy конфигурации.
- **Достоверность Lua-окна cost-cap против реального Redis.**
`apps/server/src/core/ai-chat/public-share-workspace-limiter.ts`
(`SLIDING_WINDOW_LUA`). Сейчас cap тестируется против TS-реализации
`FakeRedis` в `public-share-chat.spec.ts` — баг в самой Lua-строке
(`>=` vs `>`, неверный PEXPIRE) не поймается. Нужен интеграционный тест
против реального/testcontainers Redis.
## 3. Полная интеграция `AiChatService.stream` (рефактор R1-stream)
`apps/server/src/core/ai-chat/ai-chat.service.ts`. В PR #49 извлечён и
покрыт только чистый `buildErrorAssistantRecord`. Полные интеграционные
сценарии — **запись чата, упавшего на первом ходу** (onError), жизненный
цикл external-MCP клиентов (закрытие при throw/onFinish), и
**история восстанавливается из БД, а не из `body.messages`** (анти-tamper) —
требуют сидирования SDK `streamText` (инъекция/seam колбэков `onError`/
`onFinish`/`onAbort` + `res.hijack`). Отложено, чтобы не дестабилизировать
287-строчный `stream()`; делать вместе с выносом testable turn-pipeline.
---
## Сопутствующие НЕ-тестовые находки
Вынесены в отдельные issues (всплыли во время написания тестов):
- #52 — ai-roles: нет серверной валидации модели роли + дрейф enum драйверов.
- #53 — ws: `invalidateSpaceRestrictionCache` без вызывающих (30с stale-окно).
- #54 — page-embed: серверный guard глубины/циклов раскрытия.
- #55 — transclusion: cycle-guard в `collectPageEmbedsFromPmJson`.
- #56 — test-infra: jest DI + lib0 ESM (16 падающих сьютов).