# QA AI-функций AGIL Calls (заметки, compliance, in-call ассистент, knowledge, AI-агент)

Дата прогона: **2026-11-13** · стенд: **https://calls.guardify.ru** · тенант `demo` ·
окружение: Node 23, headless Chrome (puppeteer-core, fake media devices), Windows.
Зона: `qa/ai/**` (харнесс и артефакты), этот отчёт — `docs/qa-ai.md`.
Исходники продукта в рамках этого прохода **не менялись** (P0/P1 в `packages/ai`
не найдено).

## 1. Методика

Три исполняемых сьюта против живого стенда (никаких моков, кроме собственных
локальных http-серверов в unit-тестах `@agil/ai`):

| Сьют | Что покрывает | Файл |
|---|---|---|
| API AI (45 проверок) | admin API настроек AI/knowledge/in-call, SSRF-валидация, policy rules, транскрипт/заметки/compliance из seed-данных, caption→PII→notes pipeline, режимы in-call (local/external/hybrid + fallback), knowledge (denied/not-configured/upstream), STT-лимиты, аудит | [`qa/ai/api-ai.mjs`](../qa/ai/api-ai.mjs) |
| Dashboard AI (23 проверки) | вход, экран «Безопасность» → AI-агент (режимы, тумблеры, rules editor add/save/reload/remove, SSRF-ошибка), экран Knowledge-агента (статусы/роли/«Проверить»), вкладки комнаты Транскрипт/Заметки/Compliance (summary, выдержки, задачи, фильтр severity, «Запустить анализ»), наличие UI настроек in-call, иконки/перекрытия/CLS/горизонтальный overflow, мобильный вид 390px | [`qa/ai/browser-dashboard-ai.mjs`](../qa/ai/browser-dashboard-ai.mjs) |
| Widget assistant (21 проверка) | панель in-call ассистента (local), все 13 команд (микрофон/камера/экран/чат/файлы/участники/заметки/настройки/blur/minimize/fullscreen/new-tab/invite), запись (consent/старт/стоп/отказ guest), spotlight 14 логических target'ов, тур 6 шагов, Esc/a11y, knowledge-режим (ошибка/отказ), hybrid 501 fallback и external 502, mobile 390px, консоль/pageerror | [`qa/ai/browser-widget-assistant.mjs`](../qa/ai/browser-widget-assistant.mjs) |

Общие утилиты (API с backoff на admin rate-limit, браузер, скриншоты, CLS,
аудит верстки) — [`qa/ai/lib.mjs`](../qa/ai/lib.mjs); сводка —
[`qa/ai/summarize.mjs`](../qa/ai/summarize.mjs).

### Seed-данные

Использованы существующие 12 комнат с транскриптами/заметками (`... seed 1..12`):
транскрипт (10 сегментов, `isFinal`), `notes.source=local`, 5 выдержек, задачи,
решения, топики — прочитаны без изменений. Для активных сценариев создавались
**свои** QA-комнаты (caption-сессия через signaling, 5 реплик: email, телефон,
решение, риск, поручение со сроком), после прогона удалены. Seed-комнаты **не
удалялись**; конфиг AI-секций восстановлен, флаги фич возвращены к seed-значениям.

### Ограничения окружения

- Стенд общий (несколько QA-агентов правят один tenant config параллельно):
  конфиг AI в ходе прогонов менялся извне; харнесс пиннит нужные значения
  (`features`, `session.inCallAgent`) на время фазы и восстанавливает baseline,
  но единичные проверки помечены как зависящие от конкуренции.
- **Нет живого корпоративного AI/knowledge endpoint** (ни OpenAI, ни on-prem):
  проверены ошибки/fallback/501/502 и валидация, но не успешный ответ внешней
  модели и не рендер цитат из реального RAG. Это требует endpoint заказчика.
- `STT` на стенде не настроен (`sttConfigured:false`) → проверен 501 с подсказкой,
  а не реальная расшифровка. Браузерный STT (Web Speech) в headless не тестируется.
- Headless-специфика: consent-диалог записи показывается не всегда (в финальном
  прогоне запись стартовала без него), `window.open`/fullscreen/clipboard
  ограничены политикой браузера — проверялись soft-проверками.
- Порог CLS принят < 0.1; фактические значения на AI-экранах 0.003–0.017.

## 2. Итоги

| Сьют | Проверок | Pass | Fail | Дефектов | Скриншотов |
|---|---:|---:|---:|---:|---:|
| `api-ai` | 45 | 44 | 1 | 1 | 0 |
| `browser-dashboard-ai` | 23 | 21 | 2 | 3 | 13 |
| `browser-widget-assistant` | 21 | 18 | 3 | 5 | 28 |
| **Итого** | **89** | **83 (93%)** | **6** | **8 уникальных** | **41** |

Дефекты по severity: **P0 — 0, P1 — 0, P2 — 4, P3 — 4.** Исправлено в рамках
прохода: **0** (фиксы вне зоны `qa/ai/**` и `docs/qa-ai.md`; в `packages/ai`
P0/P1 отсутствуют). Все дефекты — **open**, переданы владельцам зон.

## 3. Дефекты

### P2

**BUG-KNOW-03 — Knowledge «не настроен» отдаётся как 403 forbidden с текстом про API-ключ/роль**
- Зона: `packages/server/src/admin-api.ts` (`knowledgeAskHandler`, маппинг
  `knowledge_agent_not_configured` → 403) + `apps/dashboard/src/errors.ts`/
  `screens/knowledge.ts` (UI уже умеет 501/`knowledge_not_configured`, но не
  получает его; 403 рендерится как «Этот API-ключ не принадлежит тенанту»).
- Шаги: `PUT /v1/admin/knowledge-agent` `{enabled:true, endpoint:"",
  requirePermission:false}` → `POST /v1/admin/knowledge/ask` `{role:"host"}` →
  на дашборде `#/knowledge` нажать «Проверить».
- Ожидаемо: 501 `knowledge_not_configured` + подсказка «укажите endpoint».
- Фактически: 403 `forbidden`, сообщение «Knowledge assistant is not available»,
  UI показывает общий заголовок «Этот API-ключ не принадлежит тенанту».
- Скриншоты: `qa/ai/screens/dashboard-knowledge-not-configured-2026-11-13T14-54-02-285Z.png`,
  `dashboard-knowledge-disabled-2026-11-13T14-54-02-285Z.png`.

**BUG-INCALL-UI — в дашборде нет экрана настроек in-call ассистента**
- Зона: `apps/dashboard` (admin API `GET/PUT /v1/admin/in-call-agent` уже есть:
  mode/endpoint/apiKeyEnv/timeoutMs/spotlight/allowedTools/maxActionsPerReply/
  denyMessage, но UI отсутствует).
- Шаги: пройти все разделы дашборда; искать «Ассистент звонка» / in-call /
  allowedTools / spotlight.
- Ожидаемо: редактор `session.inCallAgent` (как AI-agent и Knowledge).
- Фактически: ни одного экрана/формы; в аудите лишь упоминания «In-call assistant
  request», настроек нет.
- Скриншот: `qa/ai/screens/dashboard-security-ai-2026-11-13T14-54-02-285Z.png`.

**BUG-ASSIST-STOP-REC — локальный ассистент не умеет останавливать запись**
- Зона: `packages/protocol/src/assistant.ts` (`localAssistantReply`: для
  «останови запись» правило `start_recording` вырезается, но `stop_recording`
  не добавляется; при этом `stop_recording` есть в `assistantTools`/
  `allowedTools`) + widget-контроллер.
- Шаги: в звонке «начни запись» (запись идёт) → «останови запись».
- Ожидаемо: действие `stop_recording`, запись останавливается.
- Фактически: «Не понял команду…», `tags=[]`, запись продолжается
  (`recordingActive=true`).
- Скриншот: `qa/ai/screens/widget-assistant-recording-2026-11-13T14-55-00-090Z.png`.

**BUG-SPOT-FOCUS — spotlight не переводит фокус на карточку-подсказку**
- Зона: `packages/widget/src/ui/spotlight.ts` (`this.card.focus?.()`, но `div`
  без `tabindex` — фокус не перемещается).
- Шаги: «где микрофон?» (и любой spotlight) → проверить `document.activeElement`.
- Ожидаемо: фокус в карточке (заявлено в `docs/in-call-agent.md` §3, клавиатурная
  навигация доступна).
- Фактически: `role=dialog`, `aria-live=polite` есть, но `focus=false` для всех
  14 target'ов (фокус остаётся на body/предыдущем элементе).
- Скриншот: `qa/ai/screens/widget-spotlight-mic-2026-11-13T14-55-00-090Z.png`.

### P3

**BUG-SPOT-FULLSCREEN — «где полный экран?» подсвечивает демонстрацию экрана**
- Зона: `packages/protocol/src/assistant.ts` (`INTENT_RULES`: правило
  `toggle_screen_share` с ключевым словом «экран» стоит раньше
  `request_fullscreen` с «полный экран»).
- Шаги: «где полный экран?».
- Ожидаемо: `spotlight target=fullscreen`.
- Фактически: `spotlight target=screen` (кнопка шаринга экрана).
- Скриншот: `qa/ai/screens/widget-spotlight-fullscreen-2026-11-13T14-55-00-090Z.png`.

**BUG-KNOW-GUEST-BTN — кнопка «Спросить ассистента» включена у guest без доступа, но панель не открывается**
- Зона: `packages/widget` (controls/app: знание разрешено только для host/
  moderator через `allowedRoles`, но кнопка остаётся enabled).
- Шаги: knowledge включён (`allowedRoles:host,moderator`), гость нажимает
  «Спросить ассистента».
- Ожидаемо: кнопка disabled/скрыта или панель с сообщением об отказе.
- Фактически: кнопка enabled, клик не открывает панель, обратной связи нет
  (запрос на сервер не уходит).
- Скриншот: `qa/ai/screens/widget-knowledge-guest-denied-2026-11-13T14-55-00-090Z.png`.

**BUG-KNOW-RAW-ERROR — панель knowledge-ассистента показывает сырой код ошибки**
- Зона: `packages/widget/src/ui/assistant-panel.ts` (`renderEntry`: при
  `entry.error` выводится сырая строка, напр. `internal_error`; для `forbidden`
  есть локализованный текст, для остальных — нет).
- Шаги: knowledge endpoint отвечает ошибкой (в тесте — недоступный upstream) →
  задать вопрос в панели.
- Ожидаемо: человекочитаемое сообщение с подсказкой (как `assistantExternalFailed`).
- Фактически: warning «internal_error».
- Скриншот: `qa/ai/screens/widget-knowledge-host-error-2026-11-13T14-55-00-090Z.png`.

**BUG-ROOM-EXPORT — нет экспорта заметок/compliance в дашборде**
- Зона: `apps/dashboard/src/screens/room.ts` (вкладки «Заметки»/«Compliance»:
  только «Обновить»/«Запустить анализ», нет «Экспорт/Скачать JSON/MD»).
- Шаги: комната → «Заметки» (и «Compliance») → искать экспорт.
- Ожидаемо: экспорт заметок/отчёта (JSON/MD/CSV) — в брифе «фильтры/экспорт».
- Фактически: кнопок экспорта нет.
- Скриншот: `qa/ai/screens/dashboard-room-notes-2026-11-13T14-54-02-285Z.png`.

### Наблюдение (не включено в счёт дефектов)

- **Аудит analysis.run хранит сырой текст ошибки провайдера** (до 300 символов,
  в тесте — HTML-страница upstream) в `details.externalError`. Это диагностика,
  но шумно и потенциально содержит данные провайдера: стоит сохранять только
  код/статус. Зона: `packages/server/src/analysis.ts` (+ сообщение
  `AiAgentClient` добавляет body-hint). P3, отдельного тикета не заводили.

## 4. Что прошло (ключевые сценарии)

**Compliance/заметки (интеграция `@agil/ai` через сервер):**
- локальные правила `phrase`/`regex`/`pii` (email, телефон, карта-Luhn,
  СНИЛС/ИНН) на caption-транскрипте: findings с excerpt/severity/action/source/
  participant/recommendation, дедуп;
- заметки `LocalNotesExtractor`: summary, 5 дословных выдержек (**каждая —
  подстрока транскрипта**, проверено), action items с owner и due («до 15.03»,
  «до пятницы»), decisions/risks/topics/sentiment, `source=local`;
- отчёт analysis (segmentsAnalyzed, provider), аудит `analysis.run`/
  `compliance.finding`, broadcast;
- внешний AI-агент недоступен → pipeline **деградирует в local**, заметки и
  findings сохраняются (никогда не ломает звонок);
- seed-комнаты: заметки и compliance-данные читаются без ошибок.

**Настройки и безопасность:**
- GET/PUT `/v1/admin/ai-agent`, `/knowledge-agent`, `/in-call-agent` — схемы,
  дефолты, `endpointConfigured`/`apiKeyConfigured`;
- валидация: невалидный mode/apiKeyEnv → 400; SSRF (metadata 169.254, private
  10.x, loopback, URL с credentials) → 400 `validation_failed`; UI-сохранение
  SSRF-endpoint отклоняется и не персистится;
- rules editor: add → save (4 правила в API) → reload сохраняет → remove/save
  уменьшает список; тумблеры 7 шт., chips «Endpoint не задан / Ключ не найден»;
- knowledge UI: статусы, 3 роли, allowedRoles, ask block; denied → warning
  (в т.ч. понятный текст для guest через сервер); вопрос >2000 → 400;
- in-call API: local → 501 `assistant_local_only`; external unreachable → 502
  `provider_failed`; **hybrid → 501 fallback**; disabled → 403; без токена → 401;
  текст >2000 → 400; аудит `assistant.usage` без текста промпта;
- STT: без токена → 401; не настроен → 501 `stt_not_configured` с подсказкой
  `AGIL_STT_URL`; неподдерживаемый mime/язык не роняют сервер.

**Заметки/Compliance в дашборде:** вкладки Транскрипт (5 сегментов), Заметки
(summary, `Источник: local`, выдержки, Задачи, Решения, Риски, Топики),
Compliance (3 findings, фильтр all/warn/critical, empty-state), «Запустить
анализ» (200, updated), иконки — SVG без битых изображений, CLS < 0.1,
нет горизонтального overflow/перекрытий на 1440px и 390px.

**In-call ассистент (локальный движок) в виджете:** панель `mode=in-call`,
подсказка/placeholder; **13/13 команд** возвращают ожидаемый tool и исполняются
(mic/camera реально переключаются, open_* открывают панели, screen-share
запускается, blur/minimize/restore/fullscreen/new-tab/invite выполняются);
guest «включи запись» → отказ по правам; spotlight **13/14 target'ов** и тур
6 шагов с Next/ArrowRight/«Понятно», Esc закрывает и снимает подсветку, карточка
не выходит за viewport (в т.ч. 390px); hybrid (`endpoint` недоступен) →
сервер 501 → **локальный fallback**, действие выполняется; external → 502 и
понятное сообщение «Агент компании недоступен…».

## 5. Артефакты

- Харнесс: `qa/ai/{lib,api-ai,browser-dashboard-ai,browser-widget-assistant,summarize,explore}.mjs`.
- Результаты: `qa/ai/results/{api-ai,browser-dashboard-ai,browser-widget-assistant,summary}-latest.json`
  (+ timestamped история каждого прогона).
- Скриншоты: `qa/ai/screens/**` (финальный прогон — 41 файл: 12–13 dashboard,
  28 widget; в каталоге история предыдущих итераций).
- Отчёт: `docs/qa-ai.md`.

Перезапуск (ключ админа не хранится в репозитории):

```bash
export AGIL_ADMIN_KEY=$(ssh root@95.182.94.7 "grep -oP '(?<=ADMIN_KEY=).*' /root/agil-admin-key.txt")
node qa/ai/api-ai.mjs
node qa/ai/browser-dashboard-ai.mjs
node qa/ai/browser-widget-assistant.mjs
node qa/ai/summarize.mjs
```

Скрипты идемпотентны по данным: создают собственные комнаты и удаляют их,
восстанавливают AI-секции tenant config, не трогают seed-комнаты.

## 6. Вывод по `@agil/ai`

Пакет в составе живого продукта проверен по контракту §13/§18 (и расширения
in-call/LLM): локальные правила, заметки, пайплайн анализа, HMAC-подпись,
таймауты/ретраи/валидация, fallback — **дефектов P0/P1 не выявлено**, правки в
`packages/ai` не требовались. Открытые P2/P3 относятся к серверу (`admin-api`,
`analysis`), дашборду и виджету/`protocol`-ассистенту (владельцы: backend,
dashboard/frontend, protocol/widget).

---

## Серверные исправления по findings этого отчёта (backend)

Дата: 2026-11-13 · зона `packages/server` · все прогоны локальные зелёные.

| ID | Severity | Статус | Что изменено | Проверка |
|---|---|---|---|---|
| **BUG-KNOW-03** | P2 | ✅ fixed | Knowledge «не настроен» больше не отдаёт 403/forbidden: если агент выключен, нет endpoint или не задан env-ключ — `POST /v1/admin/knowledge/ask` возвращает `501 {code:"knowledge_not_configured"}` с человеческим сообщением («Ассистент знаний не настроен: …», в т.ч. имя отсутствующего env). В WS `assistant-query` ошибка `knowledge_not_configured` + сообщение для UI; отказ по роли/правам остаётся 403. Код добавлен в доку/ошибки и в «ожидаемые» для логов (WARN, не ERROR) | `test/integrations.test.ts` (disabled → 501; enabled без ключа → 501 с именем env; role deny → 403), WS host/guest |
| **BUG-ROOM-EXPORT** | P3 | ✅ fixed/подтверждено | Добавлен `GET /v1/admin/rooms/:id/notes` → JSON `{roomId, notes, updatedAt}`; `GET /v1/admin/rooms/:id/analysis` — JSON `AnalysisResponse`. Оба поддерживают `?format=md` (Markdown) и `&download=1` (`content-disposition: attachment`) для кнопок «Скачать» в дашборде | `test/admin.test.ts` (notes JSON, notes/analysis markdown+attachment) |
| **audit externalError** | P3 | ✅ fixed | В `admin.action` `details.kind="analysis.run"` поле `externalError` больше не содержит сырой HTML/body провайдера: только код/статус (`http_502`, `timeout`, `dns_error`, `network_error`, `invalid_response`, `provider_error`). Полный текст остаётся только в локальном warning-логе | `test/admin.test.ts` (`summarizeExternalError` unit + аудит) |

Сопутствующие изменения: OpenAPI (`docs/openapi.admin.json` — путь notes + параметры
format/download), `docs/api.md` (§8: 501 knowledge_not_configured и экспорт
заметок/анализа, таблица ошибок). Регрессия: `npm run build/typecheck` OK,
`npm test -w @agil/calls-server` 87/87, `security-audit` 32/32, `e2e --only=api` 11/11.
