test(ai-chat): упрочнение фикса MCP-зависания — покрытие defense-in-depth + параллельная сборка (#397 follow-up) #405
Reference in New Issue
Block a user
Delete Branch "fix/397-mcp-hang-hardening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Хвост постфактум-ревью фикса зависания AI-чата из-за MCP (#397). Сам фикс (
16b476a2) уже в develop и работает в проде — ревьюер подтвердил корректность; это последующее упрочнение (3 теста на непокрытые defense-in-depth ветки + 1 когерентность + уточнение комментариев). Отдельным PR, как и просил ревьюер.Тесты (
ai-chat.service.setup-abort.spec.ts):close). Единственный путь освобождения поздно-зарезолвленного toolset на ветке abort — раньше без теста.streamText, ран НЕ финализируется'aborted'). Весь внешний backstop раньше не проверялся.runId &&). Вместе с тестом дедлайна запирает обе половины гардаrunId && effectiveSignal.aborted.Когерентность (
external-mcp/mcp-clients.service.tsbuildEntry):Promise.allвместо последовательногоfor-await. Общее время сборки ограничено самым медленным сервером (~2×CONNECT_TIMEOUT_MS), а не суммой. При >6 серверах в таймауте последовательная сборка превышала внешний 60-сек дедлайн, инвертируя инвариант «per-server bound — основной, внешний дедлайн — backstop».Promise.allцикле в порядке серверов → прецедентность/дизамбигуация ключей тулзов, порядокoutcomes/clientsидентичны последовательной сборке; каждый сервер сохраняет свой таймаут + закрытие при сбое;perServerне может зареджектиться (не теряет все серверы).Комментарии: onLateResolve — теперь верно «отпускает lease (декремент refcount)», а не «форс-закрывает транспорт» (кэш владеет клиентами, закрывает по TTL/evict); инвариант-комментарий — под новую параллельную сборку.
How verified
jest ai-chat.service.setup-abort.spec + external-mcp(--maxWorkers=2) → 86 passed / 11 suites; три MCP-clients-спеки (connect-timeout / lease / service) зелёные.src/core/ai-chat(--maxWorkers=2) → 829 passed / 44 suites.tsc -p apps/server— чисто по затронутым файлам.onLateResolve→ тест late-close краснеет; гардif (runId)(ре-throw и на дедлайне) → тест дедлайна краснеет. Откачено.Внутренний цикл: 1 проход. Отдельно перепроверено самое рисковое — параллельная
buildEntry:perServerне реджектится (иначе потеря всех серверов), merge строго в порядке серверов (прецедентность идентична), закрытие при сбое эквивалентно, гонок общих массивов нет.Осознанные сдвиги параллелизации (не баги): одновременно открывается N connect'ов вместо по одному; порядок warn-логов недоступных серверов может не совпадать с порядком серверов (на
outcomes/сборку не влияет).Checklist
Передаю на ревью. Закрывает DO из постфактум-ревью #397: 3 теста на непокрытые defense-in-depth ветки (поздний late-close тулсета, чистый 60-сек дедлайн → Docmost-only, legacy без runId), параллелизация per-server сборки MCP (внешний дедлайн снова становится backstop, семантика merge байт-в-байт сохранена), уточнение двух комментариев. Сам фикс не тронут. Прогоны: 86/86 (setup-abort + external-mcp), 829/829 полный ai-chat, tsc чисто, обе мутации краснеют. Внутренний цикл: 1 проход, блокеров нет.
🤖 Авто-ревью — test(ai-chat): упрочнение фикса MCP-зависания (#397 follow-up) → PASS ✅
Полностью и корректно закрывает все 4 находки постфактум-ревью #397: 3 теста на defense-in-depth ветки + параллельная сборка (когерентность) + бонусом починен док-нит про
onLateResolve. Гейт зелёный (чистый клон-рум): servertsc0, setup-abort 4 passed, connect-timeout 5 passed, все external-mcp спеки 82 passed.✅ Проверено независимо (9 аспектов; тесты — мутациями с --no-cache)
--no-cache):(late)=>{close late.clients}→ падает только late-close тест (assertcloseвызван ровно раз, не «no-crash»). Залочен.effectiveSignal.aborted) → падает только deadline-тест. Запирает половинуabortedгарда.runId &&(→if (effectiveSignal.aborted)) → падает только legacy-тест. Запирает половинуrunId. Вместе тесты 2+3 запирают ОБЕ половиныrunId && effectiveSignal.aborted; исходный D2-тест держит положительный случай.buildEntry, prod-изменение) корректна. Последовательныйfor-await→perServer(server)+Promise.all, затем ПОСЛЕДОВАТЕЛЬНЫЙ merge-loop в порядкеservers(results[i]=servers[i]). Порядок детерминизма (namespacing/disambiguation/clients/outcomes/instructions) байт-в-байт как раньше; во время concurrent-фазы shared-массивы не мутируются (только локальныйclient+ возврат результата) → гонки нет; close/leak сохранены (if(client)корректен после выноса регистрации; connect-timeout сам закрывает orphan);Promise.allне reject'ит (каждый perServer ловит свою ошибку →{ok:false}). Время сборки теперь ≈ max одного сервера (~2×CONNECT_TIMEOUT ≈ 10s) вместо суммы → инверсия «per-server primary, outer backstop» при >6 таймаутящих серверах устранена.ai-chat.service.ts(+11/-4) — только комментарии (рацио concurrency + уточнение, чтоc.close()в onLateResolve = release lease-refcount, не force-close транспорта — ровно тот док-нит, что я отметил в #397). Поведение не меняется.⛔ DROP — калибровочный лог (кодеру НЕ делать)
[speculative]suggestion/low[stability] теоретически: если в merge-loop когда-нибудь добавят падающую работу, уже подключённые ok:true-клиенты утекут (ни один ещё не вCacheEntry, reject-evict вgetOrBuildEntryничего не закроет). Сегодня недостижимо —mergeNamespacedчисто-тотальная строковая операция без throw-пути. Только защитная заметка на будущее.[below-threshold]suggestion/low[test-coverage] нет прямого юнит-теста на timing-свойство (сборка ограничена max, а не суммой). Корректность черезbuildEntryпокрыта namespacing/lease/connect-timeout спеками; тайминг сложно юнит-тестировать, реальные гарантии — per-server bound + 60с deadline.🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт (чистый клон-рум): tsc 0 · setup-abort jest 4 · connect-timeout 5 · external-mcp 82. Независимо: мутации onLateResolve/deadline/legacy (
--no-cache, каждая роняет свой тест; «1-2 failed» без --no-cache — stale jest-кэш, артефакт). База developf68c7ba7.