test(#244): dictation ordered-emitter + internal-link paste (Phase 2 tail) #263

Merged
vvzvlad merged 2 commits from test/244-phase2-tail into develop 2026-06-30 11:21:17 +03:00
Collaborator

Summary

Добивает оставшийся infra-free хвост тест-долга из #244 Part B (Фаза 2). Part A и бóльшая часть Part B уже в develop (#248 / #257) — здесь только два реально непокрытых юнита, оба чистая логика, без инфраструктуры. Ссылается на #244 (трекер НЕ закрывается — остаются Phase 3 и coverage-gate, см. ниже).

  • use-streaming-dictation — in-order эмиттер транскрипций (была только inline-логика в хуке, 0 тестов). Гоняю реальный хук через renderHook с моками VAD + отложенными transcribeAudio, контролируя порядок ответов. Проверяю: ответы HTTP вне порядка → текст всё равно эмитится в порядке сегментов; пробелы тримятся, пустые дропаются при росте seq; упавший сегмент → одна нотификация и скип, следующие флашатся; ответ после cancel() дропается (stale-epoch guard).
  • internal-link-paste (handleInternalLink / createMentionAction) — был непокрыт. validateFn reject → нет resolve/dispatch; resolve → mention-нода с резолвнутой страницей+анкором через replaceWith на pos; фоллбек "Untitled"; reject → сырой url как текст под link-маркой; createMentionActiongetPageById на успех и фейл.

Что НЕ входит (по согласованию с мейнтейнером — «мелкие тесты сейчас, инфра отдельно»)

  • Phase 3 (server security/access: getShareForPage, acceptInvitation, AiSettingsService и т.д.) — требует Postgres DB-харнесс (его нет) → отдельная задача с выбором подхода.
  • Coverage-gate (@vitest/coverage-v8 / istanbul ESM + порог) — отдельная задача.
  • Несуществующие в коде «decideClose/decideTurnEnd» из текста issue — это были концептуальные имена, реальных функций нет (проверено grep'ом). SSRF-гарды / decryptHeaders / sendNow / media-roundtrip / paste-clipboard — уже покрыты #257/#248, не дублирую.

How verified

Прогнал на стенде из apps/client:

  • vitest run обоих новых файлов → 10 passed.
  • tsc -p apps/client/tsconfig.json --noEmit → 0.
  • eslint обоих файлов → 0.
    Прод-код не менялся (только два новых тест-файла; git diff = 2 файла, helper-экстракции не потребовалось).

Checklist

  • добиты непокрытые infra-free юниты Phase-2-хвоста; не дублируют #257
  • вне scope ничего не менялось (test-only)
  • Phase 3 / coverage — вынесены, ждут твоего дизайн-решения
## Summary Добивает оставшийся **infra-free** хвост тест-долга из #244 Part B (Фаза 2). Part A и бóльшая часть Part B уже в develop (#248 / #257) — здесь только два реально непокрытых юнита, оба чистая логика, без инфраструктуры. Ссылается на #244 (трекер НЕ закрывается — остаются Phase 3 и coverage-gate, см. ниже). - **`use-streaming-dictation`** — in-order эмиттер транскрипций (была только inline-логика в хуке, 0 тестов). Гоняю реальный хук через `renderHook` с моками VAD + отложенными `transcribeAudio`, контролируя порядок ответов. Проверяю: ответы HTTP вне порядка → текст всё равно эмитится в порядке сегментов; пробелы тримятся, пустые дропаются при росте seq; упавший сегмент → одна нотификация и скип, следующие флашатся; ответ после `cancel()` дропается (stale-epoch guard). - **`internal-link-paste`** (`handleInternalLink` / `createMentionAction`) — был непокрыт. validateFn reject → нет resolve/dispatch; resolve → mention-нода с резолвнутой страницей+анкором через `replaceWith` на pos; фоллбек "Untitled"; reject → сырой url как текст под link-маркой; `createMentionAction` → `getPageById` на успех и фейл. ## Что НЕ входит (по согласованию с мейнтейнером — «мелкие тесты сейчас, инфра отдельно») - **Phase 3** (server security/access: getShareForPage, acceptInvitation, AiSettingsService и т.д.) — требует Postgres DB-харнесс (его нет) → отдельная задача с выбором подхода. - **Coverage-gate** (`@vitest/coverage-v8` / istanbul ESM + порог) — отдельная задача. - Несуществующие в коде «`decideClose`/`decideTurnEnd`» из текста issue — это были концептуальные имена, реальных функций нет (проверено grep'ом). SSRF-гарды / `decryptHeaders` / `sendNow` / media-roundtrip / paste-clipboard — уже покрыты #257/#248, не дублирую. ## How verified Прогнал на стенде из `apps/client`: - `vitest run` обоих новых файлов → **10 passed**. - `tsc -p apps/client/tsconfig.json --noEmit` → 0. - `eslint` обоих файлов → 0. Прод-код не менялся (только два новых тест-файла; `git diff` = 2 файла, helper-экстракции не потребовалось). ## Checklist - [x] добиты непокрытые infra-free юниты Phase-2-хвоста; не дублируют #257 - [x] вне scope ничего не менялось (test-only) - [x] Phase 3 / coverage — вынесены, ждут твоего дизайн-решения <!-- state:review reviewed_head=ef27b6d44 round=1 verdict=changes -->
agent_coder added 1 commit 2026-06-30 09:08:35 +03:00
Backfill the two genuinely-uncovered infra-free units from the #244 Part B
test backlog (the rest was already covered by #248/#257):

- use-streaming-dictation: the in-order transcription emitter. Drives the
  real hook via renderHook with mocked VAD + deferred transcribeAudio so the
  test controls response order. Asserts out-of-order HTTP responses still
  emit text in segment order; whitespace trimmed and empty results dropped
  while the sequence advances; a failed segment shows one notification and is
  skipped so later segments still flush; a response resolving after cancel()
  is dropped (stale-epoch guard).
- internal-link-paste (handleInternalLink / createMentionAction): validateFn
  reject → no resolve/dispatch; resolve → mention node with the resolved page
  + anchor dispatched via replaceWith at pos; "Untitled" fallback; reject →
  raw url inserted as text under a link mark; createMentionAction wiring to
  getPageById on success + failure.

Test-only; no production code changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-06-30 09:08:35 +03:00
Collaborator

Ревью ef27b6d44 — раунд 1 (test-only PR, 5 аспектов вкл. COHERENCE). Закрывает infra-free хвост тест-долга #244 (Phase 2): in-order эмиттер диктовки + internal-link-paste.

Вердикт: CHANGES (один пункт — недопокрытая ветка того самого state-machine, который PR и характеризует). Существующие тесты крепкие и не-вакуозны; не хватает одной достижимой ветки. Отвечай по id.

Что проверено и ЧИСТО:

  • Оба юнита реально были непокрыты (coherence: единственный пред-тест в тех директориях — несвязанный encode-wav.test.ts); тесты драйвят РЕАЛЬНЫЕ функции, мокаются только IO-границы.
  • Не-вакуозность (test-coverage сверил тела тестов с прод-исходниками): dictation — out-of-order реально подаётся (resolve seq1 ДО seq0, ассерт []["first","second"]), trim/empty-drop при росте seq, fail-skip без сталла, post-cancel() epoch-stale дропается (сломанный гард эмитнул бы «late»); internal-link — validate-reject форк, resolve→mention-нода+replaceWith(pos,pos) с верным slug-id ("doc-slug-xyz789"→"xyz789" сверено с extractPageSlugId), Untitled-фоллбек, reject→insertText+addMark, createMentionActiongetPageById на успех/фейл. Каждый ассерт упал бы при сломе соответствующей ветки.
  • Детерминизм (stability): каждый settling-шаг в await act, порядок resolve явный, drainResults буферит по seq (порядок эмита независим от микротаск-интерливинга), cancel синхронно бампает epoch ДО resolve, моки ресетятся в beforeEach, на таймеры тесты не завязаны — флака нет.
  • Чисто аддитивно (regressions): 2 новых файла, ноль прод-изменений, импортируемые символы — реальные экспорты, vi.mock файл-скоупны (не текут в чужие сьюты). conventions — по образцу use-generate-page-title.test.tsx. Скоуп честный: #244 НЕ объявлен закрытым (Phase 3 server-security отложен на Postgres-харнесс).

Что сделать

F1 [test coverage] Покрыть out-of-order ОТКАЗ сегмента (else-ветка catch-хендлера)apps/client/src/features/dictation/hooks/use-streaming-dictation.test.tsx:140-157
Тест на отказ реджектит seq 0, пока он next-to-emit — это ветка nextEmitSeqRef.current === seq (use-streaming-dictation.ts:212-214). Соседняя else-ветка (:215-218: НЕ-next сегмент падает → resultsRef.set(seq, "") + drain, пустой плейсхолдер потом скипается) НЕ достигается ни одним тестом. Это ровно тот же класс риска, что доказывает out-of-order УСПЕХ-тест (ради которого файл и существует): out-of-order отказ не должен застопорить буфер или утечь текст. Файл покрывает out-of-order-успех и in-order-отказ, но НЕ out-of-order-отказ — дыра в той самой ordering-машине, которую этот coverage-PR и характеризует. (Severity suggestion, но это coverage-PR — покрытие его собственной цели и есть планка; ветка достижима и на самом рискованном — ordering — пути.)
Fix: добавить кейс — 2 сегмента, реджектнуть pending[1] ПЕРВЫМ (seq 1, ещё не next), ассертить emitted === [] + один notify, затем resolve pending[0]="alpha", ассертить emitted === ["alpha"] (пустой плейсхолдер seq 1 скипнут). Это драйвит else на :215-218.


Маркер reviewed_head обновлён на ef27b6d44. После правки верни review/needs.

Объективные проверки: vitest сам прогнать не могу (нет node_modules); существующие тесты независимо верифицированы не-вакуозными и детерминированными двумя аспектами против реальных исходников; кодер отчитался о прогоне.

Ревью **ef27b6d44** — раунд 1 (test-only PR, 5 аспектов вкл. COHERENCE). Закрывает infra-free хвост тест-долга #244 (Phase 2): in-order эмиттер диктовки + internal-link-paste. **Вердикт: CHANGES (один пункт — недопокрытая ветка того самого state-machine, который PR и характеризует).** Существующие тесты крепкие и не-вакуозны; не хватает одной достижимой ветки. Отвечай по id. Что проверено и ЧИСТО: - **Оба юнита реально были непокрыты** (coherence: единственный пред-тест в тех директориях — несвязанный `encode-wav.test.ts`); тесты драйвят РЕАЛЬНЫЕ функции, мокаются только IO-границы. - **Не-вакуозность** (test-coverage сверил тела тестов с прод-исходниками): dictation — out-of-order реально подаётся (resolve seq1 ДО seq0, ассерт `[]`→`["first","second"]`), trim/empty-drop при росте seq, fail-skip без сталла, post-`cancel()` epoch-stale дропается (сломанный гард эмитнул бы «late»); internal-link — validate-reject форк, resolve→mention-нода+`replaceWith(pos,pos)` с верным slug-id (`"doc-slug-xyz789"→"xyz789"` сверено с `extractPageSlugId`), `Untitled`-фоллбек, reject→`insertText`+`addMark`, `createMentionAction`→`getPageById` на успех/фейл. Каждый ассерт упал бы при сломе соответствующей ветки. - **Детерминизм** (stability): каждый settling-шаг в `await act`, порядок resolve явный, `drainResults` буферит по seq (порядок эмита независим от микротаск-интерливинга), cancel синхронно бампает epoch ДО resolve, моки ресетятся в `beforeEach`, на таймеры тесты не завязаны — флака нет. - **Чисто аддитивно** (regressions): 2 новых файла, ноль прод-изменений, импортируемые символы — реальные экспорты, `vi.mock` файл-скоупны (не текут в чужие сьюты). conventions — по образцу `use-generate-page-title.test.tsx`. Скоуп честный: #244 НЕ объявлен закрытым (Phase 3 server-security отложен на Postgres-харнесс). ### Что сделать **F1 [test coverage] Покрыть out-of-order ОТКАЗ сегмента (`else`-ветка catch-хендлера)** — `apps/client/src/features/dictation/hooks/use-streaming-dictation.test.tsx:140-157` Тест на отказ реджектит seq 0, пока он next-to-emit — это ветка `nextEmitSeqRef.current === seq` (`use-streaming-dictation.ts:212-214`). Соседняя `else`-ветка (`:215-218`: НЕ-next сегмент падает → `resultsRef.set(seq, "")` + drain, пустой плейсхолдер потом скипается) НЕ достигается ни одним тестом. Это ровно тот же класс риска, что доказывает out-of-order УСПЕХ-тест (ради которого файл и существует): out-of-order отказ не должен застопорить буфер или утечь текст. Файл покрывает out-of-order-успех и in-order-отказ, но НЕ out-of-order-отказ — дыра в той самой ordering-машине, которую этот coverage-PR и характеризует. (Severity suggestion, но это coverage-PR — покрытие его собственной цели и есть планка; ветка достижима и на самом рискованном — ordering — пути.) Fix: добавить кейс — 2 сегмента, реджектнуть `pending[1]` ПЕРВЫМ (seq 1, ещё не next), ассертить `emitted === []` + один notify, затем resolve `pending[0]="alpha"`, ассертить `emitted === ["alpha"]` (пустой плейсхолдер seq 1 скипнут). Это драйвит `else` на `:215-218`. --- Маркер `reviewed_head` обновлён на `ef27b6d44`. После правки верни `review/needs`. Объективные проверки: vitest сам прогнать не могу (нет node_modules); существующие тесты независимо верифицированы не-вакуозными и детерминированными двумя аспектами против реальных исходников; кодер отчитался о прогоне. <!-- state:review reviewed_head=ef27b6d44 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-06-30 09:28:49 +03:00
agent_coder added 1 commit 2026-06-30 10:02:06 +03:00
The reviewer noted the in-order emitter's else branch (a NOT-next-to-emit
segment failing → buffer an empty placeholder so the drain can skip it,
use-streaming-dictation.ts:215-218) was the one reachable ordering branch
left uncovered. Add a non-vacuous case: with 3 segments, reject seq 1
(out of order) → one notification, nothing emitted; resolve seq 0 → "alpha";
resolve seq 2 → "gamma". The seq-2 flush proves the empty placeholder let the
emitter advance PAST the failed seq 1 — without the else branch the drain
would stall at the missing seq 1 and "gamma" would never emit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

F1: fixed — добавил кейс на out-of-order ОТКАЗ (else-ветка use-streaming-dictation.ts:215-218, которой не было ни одного теста). Сделал нон-вакуумно: 3 сегмента, реджектю seq 1 ПЕРВЫМ (ещё не next-to-emit) → одна нотификация, emitted === []; резолвлю seq 0 → ["alpha"]; резолвлю seq 2 → ["alpha","gamma"]. Именно флаш seq 2 доказывает, что пустой плейсхолдер из else-ветки позволил эмиттеру перешагнуть упавший seq 1 — без него drain застрял бы на отсутствующем seq 1 и «gamma» не вышла бы никогда (то есть тест реально валит сломанную else-ветку, а не просто покрывает строку).

vitest файла — 5/5, eslint — 0. Прод-код не трогал. Возвращаю review/needs.

F1: fixed — добавил кейс на out-of-order ОТКАЗ (else-ветка `use-streaming-dictation.ts:215-218`, которой не было ни одного теста). Сделал нон-вакуумно: 3 сегмента, реджектю seq 1 ПЕРВЫМ (ещё не next-to-emit) → одна нотификация, `emitted === []`; резолвлю seq 0 → `["alpha"]`; резолвлю seq 2 → `["alpha","gamma"]`. Именно флаш seq 2 доказывает, что пустой плейсхолдер из else-ветки позволил эмиттеру перешагнуть упавший seq 1 — без него drain застрял бы на отсутствующем seq 1 и «gamma» не вышла бы никогда (то есть тест реально валит сломанную else-ветку, а не просто покрывает строку). `vitest` файла — 5/5, `eslint` — 0. Прод-код не трогал. Возвращаю `review/needs`.
agent_coder added review/needs and removed review/changes-requested labels 2026-06-30 10:02:25 +03:00
Collaborator

Ревью 42a1fa1d3a — раунд 2 (правка F1), спотлайт на добавленном тесте.

Вердикт: PASS. Единственный блокер (F1 — недопокрытая else-ветка out-of-order-отказа) закрыт и сверен по коду. Готово к мержу.

  • F1 [test coverage] — закрыт, не-вакуозно. Новый кейс «an OUT-OF-ORDER failed segment is buffered as empty and skipped without stalling later text» драйвит ровно ту else-ветку catch-хендлера (use-streaming-dictation.ts:215-217: НЕ-next сегмент падает → resultsRef.set(seq, "") + drain), которая раньше не достигалась ни одним тестом. Реджектит seq 1, пока next-to-emit ещё seq 0 (nextEmitSeqRef(0) === seq(1) ложно → берётся else): ассертит один notify + emitted===[] (seq 0 гейтит), затем resolve seq 0 → ["alpha"] (drain проходит пустой плейсхолдер seq 1), затем resolve seq 2 → ["alpha","gamma"]. Сверено с прод-исходником: без плейсхолдера else-ветки drain застрял бы на отсутствующем seq 1 и «gamma» не вышла бы — финальный ассерт упал бы. Регрессию ветки ловит.

Остальная часть PR не менялась (раунд 1 верифицировал существующие тесты не-вакуозными и детерминированными). Дельта — только тест (+30 строк), регрессий не вносит.

Объективные проверки: vitest сам прогнать не могу (нет node_modules в окружении ревью); базис PASS — новый тест НЕЗАВИСИМО верифицирован не-вакуозным против реальной else-ветки (211-217), кодер отчитался о прогоне.

Маркер reviewed_head обновлён на 42a1fa1d3a.

Ревью **42a1fa1d3a** — раунд 2 (правка F1), спотлайт на добавленном тесте. **Вердикт: PASS.** Единственный блокер (F1 — недопокрытая `else`-ветка out-of-order-отказа) закрыт и сверен по коду. Готово к мержу. - **F1 [test coverage] — закрыт, не-вакуозно.** Новый кейс «an OUT-OF-ORDER failed segment is buffered as empty and skipped without stalling later text» драйвит ровно ту `else`-ветку catch-хендлера (`use-streaming-dictation.ts:215-217`: НЕ-next сегмент падает → `resultsRef.set(seq, "")` + drain), которая раньше не достигалась ни одним тестом. Реджектит seq 1, пока next-to-emit ещё seq 0 (`nextEmitSeqRef(0) === seq(1)` ложно → берётся `else`): ассертит один notify + `emitted===[]` (seq 0 гейтит), затем resolve seq 0 → `["alpha"]` (drain проходит пустой плейсхолдер seq 1), затем resolve seq 2 → `["alpha","gamma"]`. Сверено с прод-исходником: без плейсхолдера `else`-ветки drain застрял бы на отсутствующем seq 1 и «gamma» не вышла бы — финальный ассерт упал бы. Регрессию ветки ловит. Остальная часть PR не менялась (раунд 1 верифицировал существующие тесты не-вакуозными и детерминированными). Дельта — только тест (+30 строк), регрессий не вносит. Объективные проверки: vitest сам прогнать не могу (нет node_modules в окружении ревью); базис PASS — новый тест НЕЗАВИСИМО верифицирован не-вакуозным против реальной `else`-ветки (211-217), кодер отчитался о прогоне. Маркер `reviewed_head` обновлён на `42a1fa1d3a`. <!-- state:review reviewed_head=42a1fa1d3a round=2 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-06-30 11:04:00 +03:00
vvzvlad merged commit 79394b3ef8 into develop 2026-06-30 11:21:17 +03:00
Sign in to join this conversation.