Батч: бейдж контекста (#189) + e2e в CI (#187) + inline-тест MCP (#170) #197

Merged
vvzvlad merged 5 commits from batch/issues-189-187-170 into develop 2026-06-26 18:09:48 +03:00
Collaborator

Батч из трёх задач, каждая — отдельным коммитом.

#189 — feat(ai-chat): бейдж контекста «текущий / максимум» в шапке

Бейдж в шапке плавающего окна AI-чата больше не «прыгает» между live-счётчиком хода и размером контекста — теперь он всегда показывает текущий / максимум (например 572 / 200k). Максимум берётся из новой настройки AI «Context window (tokens)»: сервер резолвит её и кладёт maxContextTokens в метаданные завершённого ассистентского хода (рядом с contextTokens), поэтому клиенту не нужно резолвить модель, и это переживает публичные шары / пер-ролевые модели. Live-фидбек «Thinking · N tokens» в теле чата сохранён; из шапки убран только дублирующий live-счётчик. Подводный камень: провайдерные настройки хранятся как ::text, поэтому значение коэрсится обратно в положительное целое в resolve()/getMasked().
Коммит: feat(ai-chat): header badge shows current/max context, max from AI settings (#189)

#187 — ci(develop): e2e на каждый push в develop, без блокировки деплоя

В develop.yml добавлены два независимых job — e2e-server и e2e-mcp (pgvector + redis, миграции, для MCP — сборка, старт прод-сервера с REST + /collab, seed через /api/auth/setup). build остаётся needs: test и не зависит от e2e: падение e2e не блокирует публикацию образа :develop, а делает run красным и шлёт письмо GitHub автору пуша.
Коммит: ci(develop): run server + mcp e2e on every develop push without blocking deploy (#187)

#170 — feat(ai-chat): кнопка «Test» на каждой строке списка MCP-серверов

Inline-проверка подключения прямо из строки списка внешних MCP-серверов, без попапов. Строка вынесена в AiMcpServerRow со своим инстансом мутации (изоляция состояния — нет глобального мигания). Состояния: покой (Test), проверка (loading), успех (зелёная, OK · N по числу инструментов), ошибка (красная, Failed); тултип показывает список инструментов или текст ошибки. Результат сбрасывается при смене url/transport/headers. Бэкенд/сервис/мутация не менялись.
Коммит: feat(ai-chat): inline Test button per external MCP server row (#170)


Closes #189
Closes #187
Closes #170

🤖 Generated with Claude Code

Батч из трёх задач, каждая — отдельным коммитом. ### `#189` — feat(ai-chat): бейдж контекста «текущий / максимум» в шапке Бейдж в шапке плавающего окна AI-чата больше не «прыгает» между live-счётчиком хода и размером контекста — теперь он всегда показывает `текущий / максимум` (например `572 / 200k`). Максимум берётся из новой настройки AI «Context window (tokens)»: сервер резолвит её и кладёт `maxContextTokens` в метаданные завершённого ассистентского хода (рядом с `contextTokens`), поэтому клиенту не нужно резолвить модель, и это переживает публичные шары / пер-ролевые модели. Live-фидбек «Thinking · N tokens» в теле чата сохранён; из шапки убран только дублирующий live-счётчик. Подводный камень: провайдерные настройки хранятся как `::text`, поэтому значение коэрсится обратно в положительное целое в `resolve()`/`getMasked()`. Коммит: `feat(ai-chat): header badge shows current/max context, max from AI settings (#189)` ### `#187` — ci(develop): e2e на каждый push в develop, без блокировки деплоя В `develop.yml` добавлены два независимых job — `e2e-server` и `e2e-mcp` (pgvector + redis, миграции, для MCP — сборка, старт прод-сервера с REST + `/collab`, seed через `/api/auth/setup`). `build` остаётся `needs: test` и **не** зависит от e2e: падение e2e не блокирует публикацию образа `:develop`, а делает run красным и шлёт письмо GitHub автору пуша. Коммит: `ci(develop): run server + mcp e2e on every develop push without blocking deploy (#187)` ### `#170` — feat(ai-chat): кнопка «Test» на каждой строке списка MCP-серверов Inline-проверка подключения прямо из строки списка внешних MCP-серверов, без попапов. Строка вынесена в `AiMcpServerRow` со своим инстансом мутации (изоляция состояния — нет глобального мигания). Состояния: покой (`Test`), проверка (loading), успех (зелёная, `OK · N` по числу инструментов), ошибка (красная, `Failed`); тултип показывает список инструментов или текст ошибки. Результат сбрасывается при смене url/transport/headers. Бэкенд/сервис/мутация не менялись. Коммит: `feat(ai-chat): inline Test button per external MCP server row (#170)` --- Closes #189 Closes #187 Closes #170 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude_code added 3 commits 2026-06-25 22:41:36 +03:00
Add two independent jobs to develop.yml — e2e-server and e2e-mcp — that run on
each push to develop alongside test/build. `build` stays `needs: test` only, so
a failing e2e never blocks the :develop image build/publish; the red run plus
GitHub's email to the pusher is the notification.

- e2e-server: pgvector + redis services, migrations, apps/server test:e2e.
- e2e-mcp: build editor-ext/server/mcp, migrate, start the prod server
  (REST + /collab in one process), wait for /api/health, seed the admin via
  /api/auth/setup, then run @docmost/mcp test:e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a per-row Test button to the external MCP servers list that shows the
connection result inline (no toasts). Extract the row into AiMcpServerRow so
each row owns its own useTestAiMcpServerMutation instance — independent loading
and result, no cross-row flicker.

States: idle (Test), pending (loading), success (green, "OK · N" with the tool
count), failure (red, "Failed"); a tooltip shows the tool list or the error.
The result resets when url/transport/headers change (the row is keyed by id, so
it does not remount). Backend, service and mutation are unchanged.

- ai-mcp-servers.tsx: AiMcpServerRow + Test button + reset effect + tooltip.
- i18n: add Failed / "OK · {{n}}" (en, ru) and ru Test / tool-list keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The floating chat window's header badge flipped meaning — a live per-turn token
counter while streaming, the persisted context size at rest — so it "reset to 1"
on each prompt and conflated two different numbers. Replace it with a stable
"current / max" context badge (e.g. `572 / 200k`). The live "Thinking · N tokens"
inside the chat body stays; only the duplicate live counter is removed from the
header.

Max comes from a new admin setting "Context window (tokens)". The server resolves
it and attaches `maxContextTokens` to the completed assistant turn's metadata
(next to contextTokens), so the badge needs no client-side model resolution and
this survives public shares / per-role models.

Server:
- ai.types: chatContextWindow on AiProviderSettings + PROVIDER_SETTINGS_KEYS +
  ResolvedAiConfig + MaskedAiSettings.
- workspace.repo: chatContextWindow in AI_PROVIDER_SETTINGS_ALLOWED (parity).
- update-ai-settings.dto: @IsInt @Min(0) chatContextWindow.
- ai-settings.service: coerce the ::text-stored value to a positive int in
  resolve()/getMasked().
- ai-chat.service: flushAssistant writes metadata.maxContextTokens (>0); the
  completed turn passes resolved.chatContextWindow.

Client:
- ai-chat.types: maxContextTokens on the message-row metadata.
- ai-chat-window: read maxContextTokens; render "current [/ max]"; drop the
  liveTurnTokens state/branch and the onLiveTurnTokens prop; new tooltip.
- chat-thread: remove the live-turn-token throttle effect and plumbing.
- count-stream-tokens: drop the now-dead liveTurnTokens()/types; keep
  estimateTokens.
- settings: chatContextWindow on IAiSettings(+Update) + a NumberInput in the AI
  provider settings form.

i18n: add the badge/settings keys (en, ru); remove the two now-unused keys.
Tests: flushAssistant maxContextTokens, DTO validation, trim token tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Collaborator

Code review — батч #197 (бейдж контекста #189 + e2e в CI #187 + inline-тест MCP #170)

Вердикт: Approve with comments (одобрено с замечаниями). Блокирующих проблем нет. Удаление старого live-счётчика токенов из шапки выполнено чисто (ни одной висячей ссылки на удалённые символы), новое поле chatContextWindow валидируется и проброшено консистентно во все слои, новые CI-джобы аддитивны и не гейтят деплой. Замечания ниже некритичные; главное из них — отсутствие тестов на парсинг ::text для chatContextWindow.

Ревью провёл в 8 аспектов параллельно (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture) против merge-base с develop. Объём: 18 файлов, +481/−390. Аспекты security, conventions, documentation, regressions и architecture — чисто (LGTM), все находки верифицированы по ветке batch/issues-189-187-170.

Must fix before merge

Нет.

Test coverage

  • [warning] Покрыть тестом коэрсинг chatContextWindow из ::text в resolve() и getMasked()apps/server/src/integrations/ai/ai-settings.service.ts:162-176, 235-241. Блок Number(provider.chatContextWindow); Number.isFinite && >0 ? Math.floor : undefined — реальный парсинг с 4 ветками, не покрыт ни одним тестом: отдельного ai-settings.service.spec.ts нет, и ни один спек не инстанцирует AiSettingsService/не вызывает resolve()/getMasked() (проверено по ветке). Непокрытые ветки: валидная строка "200000"200000; нечисловая/пустая ""/"abc"undefined; "0"/отрицательная→undefined; дробная "1.5"Math.floor1. Блок продублирован в getMasked() с теми же непокрытыми ветками. Fix: добавить ai-settings.service.spec.ts, который стабает readProvider под каждую форму и проверяет resolve().chatContextWindow / getMasked().chatContextWindow (200000 / undefined / undefined / 1).
  • [warning] Покрыть выбор maxContextTokens и ветки рендера бейджаapps/client/src/features/ai-chat/components/ai-chat-window.tsx:304-323. useMemo (скан строк с конца до самой свежей с положительным maxContextTokens, пропуск null-метаданных) и рендер бейджа (только числитель / current / max / скрыт) не покрыты. Важные кейсы: свежая строка с maxContextTokens: 0 не должна перекрывать более старую положительную; current > max показывается как есть (без clamp, по комментарию в коде). Проект тестирует подобные компоненты через RTL (role-cards, reasoning-block) — то есть это в рамках его планки.
  • [warning] Покрыть state-машину кнопки Test и сброс результатаapps/client/src/features/workspace/components/settings/components/ai-mcp-servers.tsx:167-280. Четыре состояния кнопки (idle/loading/ok/failed), текст тултипа (tools.join / "No tools available" / error) и useEffect, сбрасывающий результат при смене url/transport/hasHeaders, не покрыты. Легче всего незаметно сломать ветку ok && tools.length === 0 → "No tools available" и сброс по смене url.

Покрыто адекватно (замечаний нет): flushAssistant + maxContextTokens (set / unset / явный 0 → ключ опущен), DTO chatContextWindow (accept 0/200000, reject −1, reject 1.5, accept omitted), estimateTokens.

Non-blocking findings

  • [suggestion][stability] Сохранять вывод фонового prod-сервера в e2e-mcp для диагностируемости.github/workflows/develop.yml:132-145. start:prod & запускается без редиректа stdout/stderr. Механика верна: фоновый процесс переживает переход между шагами джобы, а упавший старт ловится health-loop'ом (exit 1 после 60 попыток). Но причина падения (ошибка бинда/стек/несоответствие миграций) нигде не сохраняется — падение CI оказывается недиагностируемым, что подрывает сам смысл джобы (красный run как уведомление). Fix: pnpm --filter ./apps/server start:prod > /tmp/server.log 2>&1 & + шаг с if: failure() и cat /tmp/server.log.
  • [suggestion][stability] Учесть, что результат Test не сбрасывается при правке только заголовковapps/client/.../ai-mcp-servers.tsx:816-819. Эффект сброса зависит от [url, transport, hasHeaders]; сами значения заголовков на клиент не отдаются (только булев hasHeaders), поэтому при смене, например, токена без смены url/transport/hasHeaders остаётся устаревший зелёный «OK · N». Это stale-positive, пользователь может перенажать Test — влияние низкое; это ограничение write-only модели заголовков, а не баг эффекта (render-loop/stale-closure нет). Опционально: сбрасывать результат при любом сохранении формы редактирования.
  • [suggestion][simplification] Вынести дублированный коэрсинг ::text→положительный int в хелперapps/server/src/integrations/ai/ai-settings.service.ts:162-176, 235-241. Идентичный 4-строчный блок в resolve() и getMasked(); в этом же модуле уже есть именованные хелперы такой формы (positiveEnv в ai-streaming-fetch.ts, embeddingTimeoutMs в ai.service.ts), так что хелпер — норма модуля, а не новая абстракция, и устраняет риск расхождения двух копий. Бонус: один хелпер проще покрыть тестом (см. Test coverage). Fix: function positiveIntOrUndefined(raw: unknown): number | undefined { const n = Number(raw); return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined; }.
  • [suggestion][simplification] Вынести повторяющийся setup в composite action.github/workflows/develop.yml:15-158. Пять шагов (Checkout / pnpm / Node / install / build-editor-ext) повторяются в e2e-server, e2e-mcp и уже в reusable test.yml — фактически трижды. Оговорка: GitHub Actions не поддерживает YAML-якоря в workflow-файлах, а services: нельзя вынести в composite action, так что блоки env:/services: останутся продублированными per-job; выносится только последовательность шагов в .github/actions/setup. Если дублирование сочтено приемлемым — можно пропустить.
  • [suggestion][simplification] Свести две параллельные if/else-цепочки в AiMcpServerRow к одной деривацииapps/client/.../ai-mcp-servers.tsx:821-848. tooltipLabel и четыре button*-переменные вычисляются двумя отдельными цепочками if/else if по одному и тому же тристейту (нет результата / ok / failed). Одна деривация (объект view с {color, variant, icon, label, tooltip}) читается легче и не даёт цепочкам разъезжаться. Чисто читаемость.

Architecture & design (forward-looking, non-blocking)

Нет. Фан-аут добавления одной настройки по ~7 спискам/типам (DTO, UpdateAiSettingsInput, AiProviderSettings, ResolvedAiConfig, MaskedAiSettings, PROVIDER_SETTINGS_KEYS, AI_PROVIDER_SETTINGS_ALLOWED) — осознанный type-safe дизайн: PROVIDER_SETTINGS_KEYSsatisfies) — канонический источник, копия AI_PROVIDER_SETTINGS_ALLOWED защищена parity-тестом в CI, а раздельные view-типы (resolved добавляет расшифрованные ключи; masked заменяет их на hasApiKey-флаги) несут разные формы. Структура впитала изменение штатно — предложений нет.

## Code review — батч #197 (бейдж контекста #189 + e2e в CI #187 + inline-тест MCP #170) **Вердикт: Approve with comments (одобрено с замечаниями).** Блокирующих проблем нет. Удаление старого live-счётчика токенов из шапки выполнено чисто (ни одной висячей ссылки на удалённые символы), новое поле `chatContextWindow` валидируется и проброшено консистентно во все слои, новые CI-джобы аддитивны и не гейтят деплой. Замечания ниже некритичные; главное из них — отсутствие тестов на парсинг `::text` для `chatContextWindow`. Ревью провёл в 8 аспектов параллельно (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture) против merge-base с `develop`. Объём: 18 файлов, +481/−390. Аспекты security, conventions, documentation, regressions и architecture — чисто (LGTM), все находки верифицированы по ветке `batch/issues-189-187-170`. ### Must fix before merge Нет. ### Test coverage - **[warning] Покрыть тестом коэрсинг `chatContextWindow` из `::text` в `resolve()` и `getMasked()`** — `apps/server/src/integrations/ai/ai-settings.service.ts:162-176, 235-241`. Блок `Number(provider.chatContextWindow); Number.isFinite && >0 ? Math.floor : undefined` — реальный парсинг с 4 ветками, не покрыт ни одним тестом: отдельного `ai-settings.service.spec.ts` нет, и ни один спек не инстанцирует `AiSettingsService`/не вызывает `resolve()`/`getMasked()` (проверено по ветке). Непокрытые ветки: валидная строка `"200000"`→`200000`; нечисловая/пустая `""`/`"abc"`→`undefined`; `"0"`/отрицательная→`undefined`; дробная `"1.5"`→`Math.floor`→`1`. Блок продублирован в `getMasked()` с теми же непокрытыми ветками. Fix: добавить `ai-settings.service.spec.ts`, который стабает `readProvider` под каждую форму и проверяет `resolve().chatContextWindow` / `getMasked().chatContextWindow` (`200000` / `undefined` / `undefined` / `1`). - **[warning] Покрыть выбор `maxContextTokens` и ветки рендера бейджа** — `apps/client/src/features/ai-chat/components/ai-chat-window.tsx:304-323`. `useMemo` (скан строк с конца до самой свежей с положительным `maxContextTokens`, пропуск null-метаданных) и рендер бейджа (только числитель / `current / max` / скрыт) не покрыты. Важные кейсы: свежая строка с `maxContextTokens: 0` не должна перекрывать более старую положительную; `current > max` показывается как есть (без clamp, по комментарию в коде). Проект тестирует подобные компоненты через RTL (`role-cards`, `reasoning-block`) — то есть это в рамках его планки. - **[warning] Покрыть state-машину кнопки Test и сброс результата** — `apps/client/src/features/workspace/components/settings/components/ai-mcp-servers.tsx:167-280`. Четыре состояния кнопки (idle/loading/ok/failed), текст тултипа (`tools.join` / "No tools available" / `error`) и `useEffect`, сбрасывающий результат при смене `url`/`transport`/`hasHeaders`, не покрыты. Легче всего незаметно сломать ветку `ok && tools.length === 0` → "No tools available" и сброс по смене `url`. Покрыто адекватно (замечаний нет): `flushAssistant` + `maxContextTokens` (set / unset / явный 0 → ключ опущен), DTO `chatContextWindow` (accept 0/200000, reject −1, reject 1.5, accept omitted), `estimateTokens`. ### Non-blocking findings - **[suggestion][stability] Сохранять вывод фонового prod-сервера в `e2e-mcp` для диагностируемости** — `.github/workflows/develop.yml:132-145`. `start:prod &` запускается без редиректа stdout/stderr. Механика верна: фоновый процесс переживает переход между шагами джобы, а упавший старт ловится health-loop'ом (`exit 1` после 60 попыток). Но причина падения (ошибка бинда/стек/несоответствие миграций) нигде не сохраняется — падение CI оказывается недиагностируемым, что подрывает сам смысл джобы (красный run как уведомление). Fix: `pnpm --filter ./apps/server start:prod > /tmp/server.log 2>&1 &` + шаг с `if: failure()` и `cat /tmp/server.log`. - **[suggestion][stability] Учесть, что результат Test не сбрасывается при правке только заголовков** — `apps/client/.../ai-mcp-servers.tsx:816-819`. Эффект сброса зависит от `[url, transport, hasHeaders]`; сами значения заголовков на клиент не отдаются (только булев `hasHeaders`), поэтому при смене, например, токена без смены url/transport/`hasHeaders` остаётся устаревший зелёный «OK · N». Это stale-*positive*, пользователь может перенажать Test — влияние низкое; это ограничение write-only модели заголовков, а не баг эффекта (render-loop/stale-closure нет). Опционально: сбрасывать результат при любом сохранении формы редактирования. - **[suggestion][simplification] Вынести дублированный коэрсинг `::text`→положительный int в хелпер** — `apps/server/src/integrations/ai/ai-settings.service.ts:162-176, 235-241`. Идентичный 4-строчный блок в `resolve()` и `getMasked()`; в этом же модуле уже есть именованные хелперы такой формы (`positiveEnv` в `ai-streaming-fetch.ts`, `embeddingTimeoutMs` в `ai.service.ts`), так что хелпер — норма модуля, а не новая абстракция, и устраняет риск расхождения двух копий. Бонус: один хелпер проще покрыть тестом (см. Test coverage). Fix: `function positiveIntOrUndefined(raw: unknown): number | undefined { const n = Number(raw); return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined; }`. - **[suggestion][simplification] Вынести повторяющийся setup в composite action** — `.github/workflows/develop.yml:15-158`. Пять шагов (Checkout / pnpm / Node / install / build-editor-ext) повторяются в `e2e-server`, `e2e-mcp` и уже в reusable `test.yml` — фактически трижды. Оговорка: GitHub Actions не поддерживает YAML-якоря в workflow-файлах, а `services:` нельзя вынести в composite action, так что блоки `env:`/`services:` останутся продублированными per-job; выносится только последовательность шагов в `.github/actions/setup`. Если дублирование сочтено приемлемым — можно пропустить. - **[suggestion][simplification] Свести две параллельные if/else-цепочки в `AiMcpServerRow` к одной деривации** — `apps/client/.../ai-mcp-servers.tsx:821-848`. `tooltipLabel` и четыре `button*`-переменные вычисляются двумя отдельными цепочками `if/else if` по одному и тому же тристейту (нет результата / ok / failed). Одна деривация (объект `view` с `{color, variant, icon, label, tooltip}`) читается легче и не даёт цепочкам разъезжаться. Чисто читаемость. ### Architecture & design (forward-looking, non-blocking) Нет. Фан-аут добавления одной настройки по ~7 спискам/типам (DTO, `UpdateAiSettingsInput`, `AiProviderSettings`, `ResolvedAiConfig`, `MaskedAiSettings`, `PROVIDER_SETTINGS_KEYS`, `AI_PROVIDER_SETTINGS_ALLOWED`) — осознанный type-safe дизайн: `PROVIDER_SETTINGS_KEYS` (с `satisfies`) — канонический источник, копия `AI_PROVIDER_SETTINGS_ALLOWED` защищена parity-тестом в CI, а раздельные view-типы (resolved добавляет расшифрованные ключи; masked заменяет их на `hasApiKey`-флаги) несут разные формы. Структура впитала изменение штатно — предложений нет.
Author
Collaborator

Code review — PR #197 «Батч: бейдж контекста (#189) + e2e в CI (#187) + inline-тест MCP (#170

Вердикт: Approve with comments (одобрить с замечаниями).

Diff (developbatch/issues-189-187-170, 18 файлов, +481/−390) разобран восемью специализированными ревью-агентами: security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture. Блокирующих проблем нет. Удаление live-счётчика токенов из шапки выполнено полностью — висячих ссылок на liveTurnTokens / onLiveTurnTokens не осталось, estimateTokens и in-body «Thinking · N tokens» сохранены; новое поле chatContextWindow консистентно проведено через все слои (DTO → service → resolve/getMasked → типы → клиентская форма) и оба allow-list, i18n-ключи добавлены в en-US и ru-RU; CI-джобы чисто аддитивны и не гейтят публикацию образа :develop. Замечания ниже — некритичные.

  • [warning][test-coverage] Покрыть юнит-тестом коэрцию chatContextWindow (::text → int) на пути чтенияapps/server/src/integrations/ai/ai-settings.service.ts:162-176 (и дубль в getMasked() :235-243). Провайдерные настройки хранятся как ::text, поэтому значение читается строкой и сворачивается в положительный int: Number(...) → Number.isFinite && > 0 ? Math.floor : undefined. Это чистая ветвистая логика ("200000"→200000, "1.9"→1, "0"→undefined, "-5"→undefined, ""/"abc"/undefined→undefined), и она не покрыта ничем — ai-settings.service.spec.ts отсутствует. Добавленный в PR DTO-тест проверяет только путь записи (@IsInt @Min(0)), а не round-trip на чтении. Регрессия (например, потеря Math.floor или > 0>= 0) уйдёт молча. Fix: вынести коэрцию в маленький экспортируемый helper parsePositiveInt(raw): number | undefined и протестировать его (заодно убирает дублирование — см. архитектурную заметку), либо добавить spec на resolve()/getMasked() с замоканным readProvider.

  • [suggestion][stability] В e2e-mcp не захватывается вывод прод-сервера и падение всплывает только через ~120 с.github/workflows/develop.yml:132-145. Шаг Start server (prod) бэкграундит сервер (pnpm … start:prod &) без перенаправления stdout/stderr; шаг завершается мгновенно, поэтому краш на старте не попадает ни в один лог, а единственный сигнал — общий «Server did not become healthy in time» после полного цикла поллинга (60×2 с). Fix: перенаправить вывод в файл (… start:prod > /tmp/server.log 2>&1 &) и cat-ить его в шаге health-check при неуспехе; опционально раньше выходить, если процесс умер.

  • [suggestion][simplification] Свести два обратных прохода по messageRows в одинapps/client/src/features/ai-chat/components/ai-chat-window.tsx:303-319. Новый useMemo для maxContextTokens повторяет форму соседнего useMemo для contextTokens (!meta-skip / typeof === "number" && > 0, та же зависимость [activeChatId, messageRows]). Их можно слить в один проход, возвращающий { contextTokens, maxContextTokens }. Важно сохранить семантику «каждое значение берётся из самого свежего ряда, где есть именно оно» (числитель и знаменатель могут лежать в разных рядах) — см. архитектурную заметку про десинхронизацию.

  • Главный пробел — серверная коэрция chatContextWindow в ai-settings.service.ts (см. warning выше): чистая, ветвистая, легко тестируемая, но без тестов.

  • Что покрыто в PR (ок): flushAssistant прикрепляет/опускает maxContextTokens (ai-chat.service.spec.ts); DTO-валидация chatContextWindow (ai-provider-settings-keys.spec.ts).

  • Удаление тестов liveTurnTokens из count-stream-tokens.test.ts оправдано — сама утилита и её единственный потребитель удалены; оставшиеся тесты estimateTokens покрывают выживший экспорт.

  • Низкий приоритет (опционально): клиентская коэрция ""→0 в buildPayload (ai-provider-settings.tsx:370-376) и выбор maxContextTokens в useMemo (ai-chat-window.tsx) — по конвенции проекта «тестируем вынесенные чистые хелперы, а не inline-мемо», ценность ниже.

  • Шов «штамповать maxContextTokens в метаданные каждого завершённого хода» vs живой конфиг. Лимит модели — это одна воркспейс-настройка (resolved.chatContextWindow), но она копируется в метаданные каждого ряда (ai-chat.service.ts:622) и восстанавливается на клиенте отдельным обратным сканом (ai-chat-window.tsx:307-320). Два следствия (ни одно не баг для v1): (1) значение «замораживается» на момент хода — при смене лимита старые чаты показывают прежний знаменатель до следующего хода; (2) числитель (contextTokens) и знаменатель (maxContextTokens) восстанавливаются двумя независимыми сканами и могут попасть на разные ряды (например, свежий error-ряд несёт contextTokens, но не maxContextTokens).
    отдавать maxContextTokens клиенту как живой конфиг* (из уже загружаемых AI-настроек / getMasked().chatContextWindow), а contextTokens оставить per-row (effort: small). Плюсы: единый источник правды, всегда актуально, уходит per-row штамп и второй скан, рассинхронизация невозможна. Минусы: связывает окно чата с settings-запросом; знаменатель становится «текущим лимитом», а не «лимитом на момент хода» (поведенческое изменение — нужно подтвердить желаемость); экспортированные транскрипты перестают самоописывать лимит.

  • Дублирование коэрции в ai-settings.service.ts. Один и тот же блок (Number(...)Number.isFinite && > 0 ? Math.floor : undefined) повторён дословно в resolve() и getMasked(). Вариант: вынести приватный/экспортируемый helper parsePositiveInt(value): number | undefined (effort: small) — единственное определение правила, тривиально тестируется (см. warning по покрытию), готово к будущим числовым ::text-настройкам. Минус — лёгкая over-DRY при всего двух местах вызова. Рекомендация: склоняюсь вынести.

## Code review — PR #197 «Батч: бейдж контекста (#189) + e2e в CI (#187) + inline-тест MCP (#170)» **Вердикт: Approve with comments** (одобрить с замечаниями). Diff (`develop`…`batch/issues-189-187-170`, 18 файлов, +481/−390) разобран восемью специализированными ревью-агентами: security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture. **Блокирующих проблем нет.** Удаление live-счётчика токенов из шапки выполнено полностью — висячих ссылок на `liveTurnTokens` / `onLiveTurnTokens` не осталось, `estimateTokens` и in-body «Thinking · N tokens» сохранены; новое поле `chatContextWindow` консистентно проведено через все слои (DTO → service → `resolve`/`getMasked` → типы → клиентская форма) и оба allow-list, i18n-ключи добавлены в `en-US` и `ru-RU`; CI-джобы чисто аддитивны и не гейтят публикацию образа `:develop`. Замечания ниже — некритичные. - **[warning][test-coverage] Покрыть юнит-тестом коэрцию `chatContextWindow` (`::text` → int) на пути чтения** — `apps/server/src/integrations/ai/ai-settings.service.ts:162-176` (и дубль в `getMasked()` `:235-243`). Провайдерные настройки хранятся как `::text`, поэтому значение читается строкой и сворачивается в положительный int: `Number(...) → Number.isFinite && > 0 ? Math.floor : undefined`. Это чистая ветвистая логика (`"200000"→200000`, `"1.9"→1`, `"0"→undefined`, `"-5"→undefined`, `""`/`"abc"`/undefined→undefined), и она не покрыта ничем — `ai-settings.service.spec.ts` отсутствует. Добавленный в PR DTO-тест проверяет только путь *записи* (`@IsInt @Min(0)`), а не round-trip на *чтении*. Регрессия (например, потеря `Math.floor` или `> 0` → `>= 0`) уйдёт молча. Fix: вынести коэрцию в маленький экспортируемый helper `parsePositiveInt(raw): number | undefined` и протестировать его (заодно убирает дублирование — см. архитектурную заметку), либо добавить spec на `resolve()`/`getMasked()` с замоканным `readProvider`. - **[suggestion][stability] В e2e-mcp не захватывается вывод прод-сервера и падение всплывает только через ~120 с** — `.github/workflows/develop.yml:132-145`. Шаг `Start server (prod)` бэкграундит сервер (`pnpm … start:prod &`) без перенаправления stdout/stderr; шаг завершается мгновенно, поэтому краш на старте не попадает ни в один лог, а единственный сигнал — общий «Server did not become healthy in time» после полного цикла поллинга (60×2 с). Fix: перенаправить вывод в файл (`… start:prod > /tmp/server.log 2>&1 &`) и `cat`-ить его в шаге health-check при неуспехе; опционально раньше выходить, если процесс умер. - **[suggestion][simplification] Свести два обратных прохода по `messageRows` в один** — `apps/client/src/features/ai-chat/components/ai-chat-window.tsx:303-319`. Новый useMemo для `maxContextTokens` повторяет форму соседнего useMemo для `contextTokens` (`!meta`-skip / `typeof === "number" && > 0`, та же зависимость `[activeChatId, messageRows]`). Их можно слить в один проход, возвращающий `{ contextTokens, maxContextTokens }`. Важно сохранить семантику «каждое значение берётся из самого свежего ряда, где есть именно оно» (числитель и знаменатель могут лежать в разных рядах) — см. архитектурную заметку про десинхронизацию. - **Главный пробел** — серверная коэрция `chatContextWindow` в `ai-settings.service.ts` (см. warning выше): чистая, ветвистая, легко тестируемая, но без тестов. - **Что покрыто в PR (ок):** `flushAssistant` прикрепляет/опускает `maxContextTokens` (`ai-chat.service.spec.ts`); DTO-валидация `chatContextWindow` (`ai-provider-settings-keys.spec.ts`). - **Удаление тестов `liveTurnTokens`** из `count-stream-tokens.test.ts` оправдано — сама утилита и её единственный потребитель удалены; оставшиеся тесты `estimateTokens` покрывают выживший экспорт. - **Низкий приоритет (опционально):** клиентская коэрция `""→0` в `buildPayload` (`ai-provider-settings.tsx:370-376`) и выбор `maxContextTokens` в useMemo (`ai-chat-window.tsx`) — по конвенции проекта «тестируем вынесенные чистые хелперы, а не inline-мемо», ценность ниже. - **Шов «штамповать `maxContextTokens` в метаданные каждого завершённого хода» vs живой конфиг.** Лимит модели — это одна воркспейс-настройка (`resolved.chatContextWindow`), но она копируется в метаданные *каждого* ряда (`ai-chat.service.ts:622`) и восстанавливается на клиенте отдельным обратным сканом (`ai-chat-window.tsx:307-320`). Два следствия (ни одно не баг для v1): (1) значение «замораживается» на момент хода — при смене лимита старые чаты показывают прежний знаменатель до следующего хода; (2) числитель (`contextTokens`) и знаменатель (`maxContextTokens`) восстанавливаются *двумя независимыми* сканами и могут попасть на разные ряды (например, свежий error-ряд несёт `contextTokens`, но не `maxContextTokens`). отдавать `maxContextTokens` клиенту как живой конфиг* (из уже загружаемых AI-настроек / `getMasked().chatContextWindow`), а `contextTokens` оставить per-row (effort: small). Плюсы: единый источник правды, всегда актуально, уходит per-row штамп и второй скан, рассинхронизация невозможна. Минусы: связывает окно чата с settings-запросом; знаменатель становится «текущим лимитом», а не «лимитом на момент хода» (поведенческое изменение — нужно подтвердить желаемость); экспортированные транскрипты перестают самоописывать лимит. - **Дублирование коэрции в `ai-settings.service.ts`.** Один и тот же блок (`Number(...)` → `Number.isFinite && > 0 ? Math.floor : undefined`) повторён дословно в `resolve()` и `getMasked()`. *Вариант:* вынести приватный/экспортируемый helper `parsePositiveInt(value): number | undefined` (effort: small) — единственное определение правила, тривиально тестируется (см. warning по покрытию), готово к будущим числовым `::text`-настройкам. Минус — лёгкая over-DRY при всего двух местах вызова. Рекомендация: склоняюсь вынести.
vvzvlad added the feature label 2026-06-26 00:31:34 +03:00
claude_code added 1 commit 2026-06-26 17:24:51 +03:00
Code-review follow-ups (Approve-with-comments) for batch #197
(context badge #189 / e2e in CI #187 / inline MCP test #170):

- server: extract the duplicated chatContextWindow ::text->positive-int
  coercion (resolve() + getMasked()) into an exported parsePositiveInt
  helper and unit-test its branches (200000/1.9/0/-5/""/abc/undefined),
  closing the untested read-path gap.
- client: merge the two backward scans over messageRows into one pure,
  exported selectContextBadge helper (numerator and denominator still
  taken from the most recent row carrying EACH value) and unit-test the
  different-rows and fresh-zero-doesn't-shadow cases.
- client: extract the MCP "Test" button tristate presentation into a pure
  mcpTestButtonView helper (collapses the two parallel if/else chains) and
  unit-test idle/ok-with-tools/ok-no-tools/failed label+tooltip branches.
- ci: redirect the backgrounded prod server's stdout/stderr to a log file
  in e2e-mcp and cat it on failure, so a start-up crash is diagnosable
  instead of surfacing only as the generic health timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude_code added 1 commit 2026-06-26 17:37:21 +03:00
The per-row MCP Test button derived its presentation solely from the test
mutation's data ({ ok, tools } | { ok, error }). When the request itself
rejected (401/403/500/network) there is no payload, so the row silently spun
back to the idle "Test" instead of reporting the failure.

Feed the mutation error into mcpTestButtonView so a reject also renders a red
"Failed", with the tooltip taken from the server message
(error.response.data.message) or a generic i18n fallback. Enable the tooltip
for any non-idle state. Cover the reject branch (with and without a server
message) in the helper unit test.

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

Code review (re-review) — PR #197: батч context badge (#189) + e2e в CI (#187) + inline MCP test (#170)

Вердикт: Approve. Дельта — это чистый test-coverage + поведенчески-нейтральный рефакторинг: две вынесенные pure-функции (selectContextBadge, mcpTestButtonView) и серверный хелпер parsePositiveInt, каждый теперь покрыт юнит-тестами, плюс захват лога сервера в CI. Новых блокеров нет, прошлые замечания закрыты.

Ре-ревью дельты 9b61024b..ba5cd024 (9 файлов, +357/−85). Аспекты: security, stability, conventions, documentation, regressions, test-coverage (параллельные ревьюеры + judge).

Статус прошлых блокеров

  • Прошлый ревью был Approve with comments без блокеров; основное замечание — нехватка тестов на новую логику бейджа/MCP-теста/числовой коэрции. Закрыто по существу: добавлены context-badge.test.ts (8 кейсов), ai-mcp-server-test-view.test.ts (4 состояния), ai-settings.service.spec.ts (8 кейсов для parsePositiveInt).
  • Риск numeric typing chatContextWindow (::text → число): семантика Number.isFinite && > 0 + Math.floor сохранена дословно, продублированный inline-код заменён единым parsePositiveInt, дрейф > 0>= 0 теперь ловится тестом. Закрыто.
  • Риск MCP external-call safety: дельта не трогает сетевой путь — выносится только презентация кнопки; тип IAiMcpServerTestResult (error уже санитизирован сервером) не изменён. Не затронуто.
  • i18n: новых строк нет — "OK · {{n}}", "No tools available", "Failed", "Test" проходят через тот же t() с уже существующими ключами. Закрыто.

Must fix before merge

Нет.

Non-blocking

Нет. (Проверено против исходников: selectContextBadge точно воспроизводит прежний двойной backward-scan с независимым подбором числителя/знаменателя и legacy-usage-fallback; metadata: {...} | null делает row(null) в тесте типобезопасным; серверный .spec.ts соответствует jest-конвенции каталога.)

Test coverage

Покрыто. Вся новая/изменённая логика дельты сопровождается тестами: бейдж (пустой ввод, оба значения из свежей строки, legacy-fallback, числитель/знаменатель с разных строк, не-затенение свежим нулём, пропуск null-metadata, без клампа); MCP-кнопка (idle/ok-с-tools/ok-без-tools/failed + ветки тултипа); parsePositiveInt (целое/дробь/0/отрицательное/пустое/нечисловое/undefined/null/number). Изменения ai-chat-window.tsx, ai-mcp-servers.tsx, ai-settings.service.ts — делегация в покрытые хелперы; CI-правка develop.yml (> /tmp/server.log 2>&1 + dump on failure) логики не несёт.

## Code review (re-review) — PR #197: батч context badge (#189) + e2e в CI (#187) + inline MCP test (#170) **Вердикт: Approve.** Дельта — это чистый test-coverage + поведенчески-нейтральный рефакторинг: две вынесенные pure-функции (`selectContextBadge`, `mcpTestButtonView`) и серверный хелпер `parsePositiveInt`, каждый теперь покрыт юнит-тестами, плюс захват лога сервера в CI. Новых блокеров нет, прошлые замечания закрыты. _Ре-ревью дельты `9b61024b..ba5cd024` (9 файлов, +357/−85). Аспекты: security, stability, conventions, documentation, regressions, test-coverage (параллельные ревьюеры + judge)._ ### Статус прошлых блокеров - Прошлый ревью был Approve with comments без блокеров; основное замечание — нехватка тестов на новую логику бейджа/MCP-теста/числовой коэрции. Закрыто по существу: добавлены `context-badge.test.ts` (8 кейсов), `ai-mcp-server-test-view.test.ts` (4 состояния), `ai-settings.service.spec.ts` (8 кейсов для `parsePositiveInt`). - Риск numeric typing `chatContextWindow` (`::text` → число): семантика `Number.isFinite && > 0` + `Math.floor` сохранена дословно, продублированный inline-код заменён единым `parsePositiveInt`, дрейф `> 0`→`>= 0` теперь ловится тестом. Закрыто. - Риск MCP external-call safety: дельта не трогает сетевой путь — выносится только презентация кнопки; тип `IAiMcpServerTestResult` (`error` уже санитизирован сервером) не изменён. Не затронуто. - i18n: новых строк нет — `"OK · {{n}}"`, `"No tools available"`, `"Failed"`, `"Test"` проходят через тот же `t()` с уже существующими ключами. Закрыто. ### Must fix before merge Нет. ### Non-blocking Нет. (Проверено против исходников: `selectContextBadge` точно воспроизводит прежний двойной backward-scan с независимым подбором числителя/знаменателя и legacy-`usage`-fallback; `metadata: {...} | null` делает `row(null)` в тесте типобезопасным; серверный `.spec.ts` соответствует jest-конвенции каталога.) ### Test coverage Покрыто. Вся новая/изменённая логика дельты сопровождается тестами: бейдж (пустой ввод, оба значения из свежей строки, legacy-fallback, числитель/знаменатель с разных строк, не-затенение свежим нулём, пропуск null-metadata, без клампа); MCP-кнопка (idle/ok-с-tools/ok-без-tools/failed + ветки тултипа); `parsePositiveInt` (целое/дробь/0/отрицательное/пустое/нечисловое/undefined/null/number). Изменения `ai-chat-window.tsx`, `ai-mcp-servers.tsx`, `ai-settings.service.ts` — делегация в покрытые хелперы; CI-правка `develop.yml` (`> /tmp/server.log 2>&1` + dump on failure) логики не несёт.
vvzvlad merged commit 580f7bd5bb into develop 2026-06-26 18:09:48 +03:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#197