Кандидаты на юнит-тесты (из gap-аудита QA-плана PR #136) #139

Closed
opened 2026-06-23 00:55:10 +03:00 by vvzvlad · 0 comments
Owner

Источник: gap-аудит ручного QA-плана из PR #136 (ветка docs/manual-qa-test-plan). Файл-первоисточник: docs/backlog/qa-plan-unit-test-candidates.md.

Чеклист (10 функций)

  • encodeWavPcm16 — WAV-кодек потоковой диктовки
  • getEmbedUrlAndProvider / getEmbedProviderById — провайдеры эмбедов
  • sanitizeUrl / isInternalFileUrl — XSS-граница ссылок/эмбедов
  • clampIndent — отступы блоков (Tab/Shift-Tab)
  • Регекс имени метки /^[a-z0-9_-][a-z0-9_~-]*$/
  • MovePageDto.position против generateJitteredKeyBetween
  • canCreatePage — гейт кнопки «New note»
  • diff → декорации + счётчики (история версий)
  • getTypesForTab против запроса репозитория уведомлений
  • Построитель to_tsquery (поиск)

Кандидаты на юнит-тесты (из gap-аудита QA-плана PR #136)

Статус: открыто. Источник — gap-аудит «забытых кейсов» ручного QA-плана
(PR #136, ветка docs/manual-qa-test-plan). Ниже — чистые/почти-чистые
функции, которые сейчас проверяются только вручную (или не проверяются вовсе).
Каждую дешевле и надёжнее покрыть юнит-тестом, а не ручным прогоном — это
единственный «пирамидный» сдвиг из всего аудита. Записи самодостаточны: target,
файл:строка, что ассертить (конкретные векторы), какой класс дефектов ловит.

Каждый пункт — отдельный *.test.ts / *.spec.ts рядом с целевым файлом
(фронт — *.test.ts через vitest, сервер — *.spec.ts через jest, как принято в проекте).


1. encodeWavPcm16 — WAV-кодек потоковой диктовки

  • Target: apps/client/src/features/dictation/utils/encode-wav.ts:3-32 (чистая, без зависимостей).
  • Класс дефектов: битый WAV-заголовок / клиппинг тихо роняет качество транскрипции по всей фиче — без видимой ошибки.
  • Ассерты:
    • Заголовок: теги RIFF/WAVE/fmt /data; fmt size = 16; audioFormat = 1 (PCM); channels = 1; bitsPerSample = 16; byteRate = sampleRate*2; blockAlign = 2; dataSize = samples.length*2.
    • Переопределение sample rate (16000 по умолчанию vs 8000/48000) записывается по offset 24.
    • Клиппинг: +1.0 → 0x7FFF (32767), -1.0 → -32768, 0 → 0, +1.5/-1.5 → рельсы; проверить асимметрию clamped < 0 ? *0x8000 : *0x7fff (нет переполнения ровно на −1.0).
    • Mono: итоговая длина blob = 44 + samples.length*2.

2. getEmbedUrlAndProvider / getEmbedProviderById — провайдеры эмбедов

  • Target: packages/editor-ext/src/lib/embed-provider.ts:8-142 (нет теста).
  • Класс дефектов: неверная capture-group → битый iframe для конкретного провайдера; ручной TC-EMBED-01 этого не ловит.
  • Ассерты: для каждого из 11 провайдеров — canonical share-URL, уже-/embed/-URL и мусорный URL:
    • YouTube watch?v=, youtu.be/, m./music.youtube-nocookie.com/embed/<id>.
    • Vimeo channel/group/album/plain → player.vimeo.com/video/<id>.
    • Loom/Airtable/Miro: already-embed проходит насквозь vs rewrite.
    • Figma id-длина {22,128}; gdrive/gsheets индекс match[4].
    • Неизвестный URL → { provider: "iframe", embedUrl: url } (ветка Iframe).

3. sanitizeUrl / isInternalFileUrl — XSS-граница ссылок/эмбедов

  • Target: packages/editor-ext/src/lib/utils.ts:385-398 (нет теста). Потребители: embed.ts, link, attachment, pdf, audio.
  • Класс дефектов: регресс здесь = stored-XSS вектор через href эмбеда/ссылки/вложения.
  • Ассерты:
    • sanitizeUrl("javascript:alert(1)")"" (обёртка маппит about:blank библиотеки в пустую строку).
    • data: / vbscript:""; https://…, относительный /api/files/…, mailto: → проходят.
    • isInternalFileUrl истинно только для префиксов /api/files/ и /files/ после trim.

4. clampIndent — отступы блоков (Tab/Shift-Tab)

  • Target: packages/editor-ext/src/lib/indent.ts:36-83 (нет теста; в плане вообще нет TC на indent).
  • Класс дефектов: ломает Tab в списках/ячейках/code-block при правке.
  • Ассерты: инкремент с clamp на 8; outdent с clamp на 0; исключённые контейнеры не трогаются; парс мусорного data-indent (non-finite / negative / >8) → clamp в min/max.

5. Регекс имени метки /^[a-z0-9_-][a-z0-9_~-]*$/

  • Target: apps/server/src/core/label/dto/create-label.dto.ts (поле name, @Matches(...)). normalizeLabelName уже покрыт — НЕ дублировать.
  • Класс дефектов: визуально-одинаковые-но-разные метки; асимметрия первого символа тихо «протухает».
  • Ассерты: accept foo, a~b, 1-2_3, -lead; reject ~lead (тильда первой), a b (пробел), héllo (unicode), пустое. Зафиксировать: ~ допустим только НЕ первым символом.

6. MovePageDto.position против generateJitteredKeyBetween

  • Target: apps/server/src/core/page/dto/move-page.dto.ts:14-16 (@MinLength(5) @MaxLength(12)) vs генератор ключа apps/server/src/core/page/services/page.service.ts:911.
  • Класс дефектов: реальный баг паритета — валидный fractional-index ключ отвергается DTO → move в глубоком дереве падает 400 (см. TC-PAGE-17).
  • Ассерты: generateJitteredKeyBetween(null,null) = "a0" (длина 2 < 5) → DTO бы отверг; серия плотных вставок between даёт ключи >12 → тоже отверг. Тест фиксирует диапазон длин ключа против границ DTO (5..12) и доказывает несовместимость → блокирует баг до фикса границ.

7. canCreatePage — гейт кнопки «New note»

  • Target: apps/client/src/features/home/components/new-note-button.tsx:17-20 (чистый предикат).
  • Класс дефектов: кнопка появляется/прячется не для тех ролей.
  • Ассерты: true только для space-роли ADMIN/WRITER; false для READER; пустой список space → false.

8. diff → декорации + счётчики (история версий)

  • Target: apps/client/src/features/page-history/components/history-editor.tsx:36-174 — вынести вычисление «old/new ProseMirror JSON → decorationSet + added/deleted/total» в чистую функцию и протестировать.
  • Класс дефектов: неверная подсветка/счётчики diff; падение на спец-нодах.
  • Ассерты: текстовая правка → inline-декорации + счётчики; добавленный image/table → целая нода; удалённый callout → widget-«призрак»; нет предыдущей версии → пустой diff, counts = 0; битая версия → fallback на plain без краша.

9. getTypesForTab против запроса репозитория уведомлений

  • Target: apps/server/src/core/notification/notification.constants.ts:49-53 (нет теста; не используется репозиторием).
  • Класс дефектов: вкладка «Direct» отдаёт verification/approval-типы (см. TC-NOTIF-06).
  • Ассерты: getTypesForTab('direct') = ровно 5 whitelisted типов; 'updates' = [PAGE_UPDATED]. Плюс контракт-тест: запрос репо (notification.repo.ts:58, сейчас type != PAGE_UPDATED) должен совпадать с whitelist — тест зафиксирует расхождение как падающий.

10. Построитель to_tsquery (поиск)

  • Target: apps/server/src/core/search/search.service.ts:37 — вынести трансформ tsquery(query.trim()+'*') и протестировать на адверсариальных входах.
  • Класс дефектов: реальный баг — запрос из операторов → необработанный to_tsquery syntax error → 500 (см. TC-SRCH-05).
  • Ассерты: входы "&", "!", "*", "<->", "\\", "the a of" (стоп-слова), пустое-после-trim, very long, unicode → безопасный пустой/нейтральный tsquery, без выброса. Гард length<1 не покрывает whitespace/операторы — тест это вскрывает.

Уже покрыто — НЕ дублировать

normalizeLabelName, resolveAudioFormat, html-embed sandbox, decideEmbedState,
статическая матрица collab-auth, persistence/history-job, zip-slip safety,
queue-helpers, chat-markdown (pending), buildPartialAssistantRecord, prepareAgentStep, showTypingIndicator.

> Источник: gap-аудит ручного QA-плана из PR #136 (ветка `docs/manual-qa-test-plan`). Файл-первоисточник: `docs/backlog/qa-plan-unit-test-candidates.md`. ## Чеклист (10 функций) - [x] `encodeWavPcm16` — WAV-кодек потоковой диктовки - [x] `getEmbedUrlAndProvider` / `getEmbedProviderById` — провайдеры эмбедов - [x] `sanitizeUrl` / `isInternalFileUrl` — XSS-граница ссылок/эмбедов - [x] `clampIndent` — отступы блоков (Tab/Shift-Tab) - [x] Регекс имени метки `/^[a-z0-9_-][a-z0-9_~-]*$/` - [x] `MovePageDto.position` против `generateJitteredKeyBetween` - [x] `canCreatePage` — гейт кнопки «New note» - [x] diff → декорации + счётчики (история версий) - [x] `getTypesForTab` против запроса репозитория уведомлений - [x] Построитель `to_tsquery` (поиск) --- # Кандидаты на юнит-тесты (из gap-аудита QA-плана PR #136) Статус: **открыто.** Источник — gap-аудит «забытых кейсов» ручного QA-плана (PR #136, ветка `docs/manual-qa-test-plan`). Ниже — чистые/почти-чистые функции, которые сейчас проверяются только вручную (или не проверяются вовсе). Каждую дешевле и надёжнее покрыть **юнит-тестом**, а не ручным прогоном — это единственный «пирамидный» сдвиг из всего аудита. Записи самодостаточны: target, `файл:строка`, что ассертить (конкретные векторы), какой класс дефектов ловит. Каждый пункт — отдельный `*.test.ts` / `*.spec.ts` рядом с целевым файлом (фронт — `*.test.ts` через vitest, сервер — `*.spec.ts` через jest, как принято в проекте). --- ## 1. `encodeWavPcm16` — WAV-кодек потоковой диктовки - **Target:** `apps/client/src/features/dictation/utils/encode-wav.ts:3-32` (чистая, без зависимостей). - **Класс дефектов:** битый WAV-заголовок / клиппинг тихо роняет качество транскрипции по всей фиче — без видимой ошибки. - **Ассерты:** - Заголовок: теги `RIFF`/`WAVE`/`fmt `/`data`; `fmt` size = 16; audioFormat = 1 (PCM); channels = 1; bitsPerSample = 16; `byteRate = sampleRate*2`; `blockAlign = 2`; `dataSize = samples.length*2`. - Переопределение sample rate (16000 по умолчанию vs 8000/48000) записывается по offset 24. - Клиппинг: `+1.0 → 0x7FFF (32767)`, `-1.0 → -32768`, `0 → 0`, `+1.5/-1.5` → рельсы; проверить асимметрию `clamped < 0 ? *0x8000 : *0x7fff` (нет переполнения ровно на −1.0). - Mono: итоговая длина blob = `44 + samples.length*2`. ## 2. `getEmbedUrlAndProvider` / `getEmbedProviderById` — провайдеры эмбедов - **Target:** `packages/editor-ext/src/lib/embed-provider.ts:8-142` (нет теста). - **Класс дефектов:** неверная capture-group → битый iframe для конкретного провайдера; ручной TC-EMBED-01 этого не ловит. - **Ассерты:** для каждого из 11 провайдеров — canonical share-URL, уже-`/embed/`-URL и мусорный URL: - YouTube `watch?v=`, `youtu.be/`, `m.`/`music.` → `youtube-nocookie.com/embed/<id>`. - Vimeo channel/group/album/plain → `player.vimeo.com/video/<id>`. - Loom/Airtable/Miro: already-embed проходит насквозь vs rewrite. - Figma id-длина `{22,128}`; gdrive/gsheets индекс `match[4]`. - Неизвестный URL → `{ provider: "iframe", embedUrl: url }` (ветка Iframe). ## 3. `sanitizeUrl` / `isInternalFileUrl` — XSS-граница ссылок/эмбедов - **Target:** `packages/editor-ext/src/lib/utils.ts:385-398` (нет теста). Потребители: `embed.ts`, link, attachment, pdf, audio. - **Класс дефектов:** регресс здесь = stored-XSS вектор через `href` эмбеда/ссылки/вложения. - **Ассерты:** - `sanitizeUrl("javascript:alert(1)")` → `""` (обёртка маппит `about:blank` библиотеки в пустую строку). - `data:` / `vbscript:` → `""`; `https://…`, относительный `/api/files/…`, `mailto:` → проходят. - `isInternalFileUrl` истинно только для префиксов `/api/files/` и `/files/` после trim. ## 4. `clampIndent` — отступы блоков (Tab/Shift-Tab) - **Target:** `packages/editor-ext/src/lib/indent.ts:36-83` (нет теста; в плане вообще нет TC на indent). - **Класс дефектов:** ломает Tab в списках/ячейках/code-block при правке. - **Ассерты:** инкремент с clamp на **8**; outdent с clamp на **0**; исключённые контейнеры не трогаются; парс мусорного `data-indent` (non-finite / negative / >8) → clamp в min/max. ## 5. Регекс имени метки `/^[a-z0-9_-][a-z0-9_~-]*$/` - **Target:** `apps/server/src/core/label/dto/create-label.dto.ts` (поле `name`, `@Matches(...)`). `normalizeLabelName` уже покрыт — НЕ дублировать. - **Класс дефектов:** визуально-одинаковые-но-разные метки; асимметрия первого символа тихо «протухает». - **Ассерты:** accept `foo`, `a~b`, `1-2_3`, `-lead`; reject `~lead` (тильда первой), `a b` (пробел), `héllo` (unicode), пустое. Зафиксировать: `~` допустим только НЕ первым символом. ## 6. `MovePageDto.position` против `generateJitteredKeyBetween` - **Target:** `apps/server/src/core/page/dto/move-page.dto.ts:14-16` (`@MinLength(5) @MaxLength(12)`) vs генератор ключа `apps/server/src/core/page/services/page.service.ts:911`. - **Класс дефектов:** **реальный баг паритета** — валидный fractional-index ключ отвергается DTO → move в глубоком дереве падает 400 (см. TC-PAGE-17). - **Ассерты:** `generateJitteredKeyBetween(null,null)` = `"a0"` (длина 2 < 5) → DTO бы отверг; серия плотных вставок between даёт ключи >12 → тоже отверг. Тест фиксирует диапазон длин ключа против границ DTO (5..12) и доказывает несовместимость → блокирует баг до фикса границ. ## 7. `canCreatePage` — гейт кнопки «New note» - **Target:** `apps/client/src/features/home/components/new-note-button.tsx:17-20` (чистый предикат). - **Класс дефектов:** кнопка появляется/прячется не для тех ролей. - **Ассерты:** true только для space-роли `ADMIN`/`WRITER`; false для `READER`; пустой список space → false. ## 8. diff → декорации + счётчики (история версий) - **Target:** `apps/client/src/features/page-history/components/history-editor.tsx:36-174` — вынести вычисление «old/new ProseMirror JSON → decorationSet + added/deleted/total» в чистую функцию и протестировать. - **Класс дефектов:** неверная подсветка/счётчики diff; падение на спец-нодах. - **Ассерты:** текстовая правка → inline-декорации + счётчики; добавленный image/table → целая нода; удалённый callout → widget-«призрак»; нет предыдущей версии → пустой diff, counts = 0; битая версия → fallback на plain без краша. ## 9. `getTypesForTab` против запроса репозитория уведомлений - **Target:** `apps/server/src/core/notification/notification.constants.ts:49-53` (нет теста; **не используется** репозиторием). - **Класс дефектов:** вкладка «Direct» отдаёт verification/approval-типы (см. TC-NOTIF-06). - **Ассерты:** `getTypesForTab('direct')` = ровно 5 whitelisted типов; `'updates'` = `[PAGE_UPDATED]`. Плюс контракт-тест: запрос репо (`notification.repo.ts:58`, сейчас `type != PAGE_UPDATED`) должен совпадать с whitelist — тест зафиксирует расхождение как падающий. ## 10. Построитель `to_tsquery` (поиск) - **Target:** `apps/server/src/core/search/search.service.ts:37` — вынести трансформ `tsquery(query.trim()+'*')` и протестировать на адверсариальных входах. - **Класс дефектов:** **реальный баг** — запрос из операторов → необработанный `to_tsquery` syntax error → 500 (см. TC-SRCH-05). - **Ассерты:** входы `"&"`, `"!"`, `"*"`, `"<->"`, `"\\"`, `"the a of"` (стоп-слова), пустое-после-trim, very long, unicode → безопасный пустой/нейтральный tsquery, без выброса. Гард `length<1` не покрывает whitespace/операторы — тест это вскрывает. --- ### Уже покрыто — НЕ дублировать `normalizeLabelName`, `resolveAudioFormat`, html-embed sandbox, `decideEmbedState`, статическая матрица collab-auth, persistence/history-job, zip-slip safety, `queue-helpers`, `chat-markdown` (pending), `buildPartialAssistantRecord`, `prepareAgentStep`, `showTypingIndicator`.
Ghost closed this issue 2026-06-24 00:44:12 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#139