fix(mcp): внятная ошибка на недоступный spaceId вместо «Space permissions not found» (#534) #536

Open
agent_coder wants to merge 4 commits from fix/534-mcp-spaceid into develop
Collaborator

Summary

MCP-инструменты с spaceId (getTree, listPages, checkNewComments, search) на валидный по форме, но несуществующий/недоступный spaceId отдавали агенту невнятное POST /pages/sidebar-pages failed (404): Space permissions not found — будто спейс есть, а прав нет. Типичная причина — опечатка в id; агент не мог себя починить. Теперь на таком 404 инструмент возвращает: «spaceId "" не найден среди доступных тебе спейсов. Доступные: (), … — скопируй нужный id из listSpaces». closes #534.

Вариант A — только обёртка packages/mcp, бэкенд не тронут (форк Docmost не дивергируем; space-ability.factory физически не различает «нет спейса» vs «нет прав» — схлопываем).

Механизм (enrich-on-404, нулевая цена happy-path)

  • getAccessibleSpaceIndex() — индекс доступных спейсов из /spaces ({ids, spaces:{id,name}[], complete}); новый paginateAllWithMeta отдаёт флаг truncated (контракт paginateAll/getSpaces/listSpaces не изменён).
  • withSpaceAccessDiagnostics(spaceId, mcpName, fn) оборачивает тела методов; на 404 сверяет spaceId с индексом и заменяет текст. Fail-open на любой неопределённости (non-404 / fetch /spaces упал / список неполон / spaceId доступен / abort) → рефроусит ОРИГИНАЛЬНУЮ серверную ошибку. Реальные ошибки не подавляются.
  • Кэш per-instance, TTL 60с (MCP_SPACES_CACHE_TTL_MS, 0=выкл); single-flight; инвалидация в тех же двух точках, что и collabTokenCache (reauth после token=null + login-success).

Ключевые решения (по резолюции владельца)

  • Abort-first по флагу сигнала toolAbortSignal?.aborted, НЕ по e.name (cap может быть TimeoutError/кастомный) — Stop/cap не переписывается в spaceId-сообщение.
  • Single-flight НИКОГДА не мемоизирует reject (занулён в .finally на resolve И reject); cachedIndex пишется только из resolved+complete — сетевой блип /spaces не отравляет фичу до конца сессии.
  • Детект 404 по HTTP-статусу, не по backend-строке (не зеркалим Space permissions not found).
  • Не обёрнуты createPage (мульти-причинный 404: parent-mismatch до проверки прав) и getPageContext (pageId). Инвариант «среди обёрнутых 404 приходит ТОЛЬКО от проверки spaceId» запинен комментом у allowlist.

How verified (на стенде)

  • Новый сьют space-access-diagnostics.test.mjs: 11/11, покрывает все 9 критериев приёмки (rewrite на битый id; happy-path без запроса /spaces — спай на client.post; TTL-кэш + рефетч; без spaceId обёртка инертна; !complete fail-open; fetch-fail fail-open + DEBUG stderr + не-мемоизация отклонённого промиса; 0 спейсов; abort-reason проброс; createPage не обёрнут).
  • Полный MCP unit-suite: 701/701; tsc чисто.
  • Внутренний цикл: 1 проход реализации + адверсариальный ревью с реальной мутацией (мемоизировал отклонённый in-flight → анти-отравляющий тест покраснел; восстановлено). Вердикт: APPROVE, блокеров/warning нет, 3 замечания информационные.

Совместимость

  • Меняется ТОЛЬКО текст ошибки на битый/недоступный spaceId. Успешные вызовы и прочие ошибки — без изменений. Риск: код, матчащий строку Space permissions not found от MCP, увидит новый текст (диагностика для агента, не контракт; голден-тесты её не матчат).
  • Новый опциональный env MCP_SPACES_CACHE_TTL_MS (дефолт 60000). Миграций нет.

Checklist

  • критерии приёмки из #534 выполнены
  • вне заявленного scope ничего не менялось
  • node_modules/миграций нет
## Summary MCP-инструменты с `spaceId` (`getTree`, `listPages`, `checkNewComments`, `search`) на **валидный по форме, но несуществующий/недоступный** spaceId отдавали агенту невнятное `POST /pages/sidebar-pages failed (404): Space permissions not found` — будто спейс есть, а прав нет. Типичная причина — опечатка в id; агент не мог себя починить. Теперь на таком 404 инструмент возвращает: «spaceId "<id>" не найден среди доступных тебе спейсов. Доступные: <id> (<name>), … — скопируй нужный id из listSpaces». closes #534. **Вариант A — только обёртка `packages/mcp`, бэкенд не тронут** (форк Docmost не дивергируем; `space-ability.factory` физически не различает «нет спейса» vs «нет прав» — схлопываем). ## Механизм (enrich-on-404, нулевая цена happy-path) - `getAccessibleSpaceIndex()` — индекс доступных спейсов из `/spaces` (`{ids, spaces:{id,name}[], complete}`); новый `paginateAllWithMeta` отдаёт флаг `truncated` (контракт `paginateAll`/`getSpaces`/`listSpaces` не изменён). - `withSpaceAccessDiagnostics(spaceId, mcpName, fn)` оборачивает тела методов; на 404 сверяет spaceId с индексом и заменяет текст. **Fail-open на любой неопределённости** (non-404 / fetch `/spaces` упал / список неполон / spaceId доступен / abort) → рефроусит ОРИГИНАЛЬНУЮ серверную ошибку. Реальные ошибки не подавляются. - **Кэш per-instance, TTL 60с** (`MCP_SPACES_CACHE_TTL_MS`, `0`=выкл); single-flight; инвалидация в тех же двух точках, что и `collabTokenCache` (reauth после `token=null` + login-success). ## Ключевые решения (по резолюции владельца) - **Abort-first по флагу сигнала** `toolAbortSignal?.aborted`, НЕ по `e.name` (cap может быть `TimeoutError`/кастомный) — Stop/cap не переписывается в spaceId-сообщение. - **Single-flight НИКОГДА не мемоизирует reject** (занулён в `.finally` на resolve И reject); `cachedIndex` пишется только из resolved+`complete` — сетевой блип `/spaces` не отравляет фичу до конца сессии. - **Детект 404 по HTTP-статусу, не по backend-строке** (не зеркалим `Space permissions not found`). - **Не обёрнуты** `createPage` (мульти-причинный 404: parent-mismatch до проверки прав) и `getPageContext` (pageId). Инвариант «среди обёрнутых 404 приходит ТОЛЬКО от проверки spaceId» запинен комментом у allowlist. ## How verified (на стенде) - Новый сьют `space-access-diagnostics.test.mjs`: **11/11**, покрывает все 9 критериев приёмки (rewrite на битый id; happy-path без запроса `/spaces` — спай на `client.post`; TTL-кэш + рефетч; без spaceId обёртка инертна; `!complete` fail-open; fetch-fail fail-open + DEBUG stderr + **не-мемоизация отклонённого промиса**; 0 спейсов; abort-reason проброс; createPage не обёрнут). - Полный MCP unit-suite: **701/701**; `tsc` чисто. - Внутренний цикл: 1 проход реализации + адверсариальный ревью с **реальной мутацией** (мемоизировал отклонённый in-flight → анти-отравляющий тест покраснел; восстановлено). Вердикт: APPROVE, блокеров/warning нет, 3 замечания информационные. ## Совместимость - Меняется ТОЛЬКО текст ошибки на битый/недоступный spaceId. Успешные вызовы и прочие ошибки — без изменений. Риск: код, матчащий строку `Space permissions not found` от MCP, увидит новый текст (диагностика для агента, не контракт; голден-тесты её не матчат). - Новый опциональный env `MCP_SPACES_CACHE_TTL_MS` (дефолт 60000). Миграций нет. ## Checklist - [x] критерии приёмки из #534 выполнены - [x] вне заявленного scope ничего не менялось - [x] node_modules/миграций нет
agent_coder added 1 commit 2026-07-12 02:34:14 +03:00
A well-formed but non-existent/inaccessible spaceId made the space-permissions
check answer with an opaque 404 ("Space permissions not found"), which the agent
could not self-correct from. Add an enrich-on-404 wrapper at the client-method
level (Variant A — no backend change) that, ONLY on that 404, replaces the server
text with a factual message naming the bad spaceId and the spaces the token can
actually see, pointing at listSpaces.

Mechanism (context.ts):
- getAccessibleSpaceIndex(): the token's accessible spaces from the single source
  of truth (/spaces), with a per-instance short-TTL cache (MCP_SPACES_CACHE_TTL_MS,
  default 60s; 0 disables) and single-flight dedup. Only a COMPLETE (untruncated)
  result is cached and only a complete result may drive the rewrite. The in-flight
  promise is nulled on BOTH resolve and reject so a transient /spaces blip is never
  memoized. Cache invalidated on every identity change, mirroring collabTokenCache
  (login() + the 401/403 reauth interceptor).
- paginateAllWithMeta(): surfaces the `truncated` flag paginateAll swallows;
  paginateAll now delegates to it (unchanged contract, getSpaces untouched).
- withSpaceAccessDiagnostics(spaceId, mcpName, fn): abort/cap wins FIRST (by the
  toolAbortSignal flag, NOT e.name — a cap may be TimeoutError/custom); only a 404
  is enrichable; FAILS OPEN (rethrows the original server error) on every source of
  uncertainty (fetch failed / !complete / spaceId present / aborted).

Wrapped only the paths where the sole 404 cause is the space membership check:
getTree, listPages (tree + recent-with-spaceId), search (with spaceId),
checkNewComments. createPage/getPageContext are deliberately NOT wrapped.
formatSpaceNotAccessible (errors.ts) composes the message (<=10 spaces + "(+N ещё)"
tail; distinct zero-spaces variant).

Tests: 11 new unit tests (stub client) covering all 9 acceptance criteria +
message shape; full mcp unit suite green (701 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-12 02:34:14 +03:00
Collaborator

Ревью — MCP enrich-on-404 для битого spaceId (#534). Вердикт: changes-requested

Механизм отличный: enrich — fail-open HINT-слой, на ЛЮБОЙ неопределённости отдаёт ОРИГИНАЛЬНУЮ 404 (abort / non-404 / фейл /spaces / truncated / id-present), happy-path 0 доп.запросов, пагинация ограничена (MAX_PAGES=50, курсор строго от сервера, break на missing/repeat), сообщение «не найден среди доступных ТЕБЕ спейсов» корректно и для несуществующего, и для нет-прав (Variant A не врёт), утечки чужих спейсов нет. Гейт mcp 843/843 + новый сьют 11/11 (мутация rewrite→passthrough роняет 3 теста — не вакуумно). Security/regressions/stability/coherence/architecture/simplification/documentation — LGTM. Эскалаций нет.

Открыто (оба мелкие, код корректен):

  • F1 — 2 fail-open гарда без тестов (мутация выживает): non-404 passthrough + id-present-edge.
  • F2 — сообщение игнорирует задокументированный в этом же файле ERROR_MESSAGE_CAP.
📋 F1, F2 — детали

F1 [warning · test-coverage] два fail-open гарда withSpaceAccessDiagnostics не покрыты — packages/mcp/src/client/context.ts:878,897

Обе ветки выживают мутацию на чистой сборке (сьют остаётся 11/11):

  1. non-404 passthrough (:878 if(!(isAxiosError && status===404)) throw e): заменить условие на false (500/403/network пошёл бы в /spaces-sweep + возможный rewrite) — не краснеет НИЧЕГО: ни один тест не подаёт non-404 ошибку в обёрнутый вызов С непустым spaceId. Это ровно регрессия, которую PR НЕ должен внести (проглотить/переформатировать реальный 500/403) — и она не заперта.
  2. id-present edge (:897 if(idx.ids.has(spaceId)) throw e): убрать → не краснеет ничего, т.к. все enrich-тесты используют BAD (никогда не в SPACES), has() всегда false. Ветка, гасящая ложное «не найден среди доступных», когда спейс НА САМОМ ДЕЛЕ доступен, не прогоняется.
    Fix: 2 теста в space-access-diagnostics.test.mjs: (a) getTree(BAD), где enumerate кидает non-404 AxiosError (status 500) при непустом spaceId → ассертить, что оригинальный 500 проходит без rewrite и c._spacesFetches===0; (b) getTree(SPACES[0].id), где enumerate кидает make404() а индекс СОДЕРЖИТ этот id → ассертить, что оригинальная 404 проходит (не /не найден среди/).

F2 [suggestion · conventions] сообщение не соблюдает ERROR_MESSAGE_CAPpackages/mcp/src/client/errors.ts:64-81

Файл документирует и enforce'ит ERROR_MESSAGE_CAP=300 (errors.ts:16,193-194) с явной целью «model context stays compact и whitelisted серверная строка не раздует текст» — применяет в formatDocmostAxiosError. Но formatSpaceNotAccessible интерполирует до 10 (SPACE_LIST_CAP) серверных name без лимита по ДЛИНЕ → ~10 длинноимённых спейсов дают строку в несколько раз больше 300ch. Единственное новое model-facing сообщение, игнорирующее собственный инвариант файла. Блэст-радиус мал (имена — свои же доступные спейсы, не атакер-инпут), потому suggestion.
Fix: применить кап к собранной строке перед возвратом (переиспользовать slice-and-ellipsis из formatDocmostAxiosError, либо усечь каждое имя).

9 аспектов (свёрнуты в 3 агента + мой разбор крюкса). Гейт: mcp build чист, node:test 843/843 + новый 11/11 (мутация rewrite→3 краснеют). База develop, own delta 6 файлов/+661. Дерево после мутаций чисто. кодеру НЕ делать: коммент read.ts:190 (парентеза о том, какой эндпоинт отдаёт 404) неточен, но функционально безвреден — below-threshold, не трогай.

## Ревью — MCP enrich-on-404 для битого spaceId (#534). Вердикт: **changes-requested** Механизм **отличный**: enrich — fail-open HINT-слой, на ЛЮБОЙ неопределённости отдаёт ОРИГИНАЛЬНУЮ 404 (abort / non-404 / фейл `/spaces` / truncated / id-present), happy-path 0 доп.запросов, пагинация ограничена (`MAX_PAGES=50`, курсор строго от сервера, break на missing/repeat), сообщение «не найден среди доступных ТЕБЕ спейсов» корректно и для несуществующего, и для нет-прав (Variant A не врёт), утечки чужих спейсов нет. Гейт mcp **843/843** + новый сьют 11/11 (мутация rewrite→passthrough роняет 3 теста — не вакуумно). Security/regressions/stability/coherence/architecture/simplification/documentation — LGTM. Эскалаций нет. Открыто (оба мелкие, код корректен): - **F1 — 2 fail-open гарда без тестов** (мутация выживает): non-404 passthrough + id-present-edge. - **F2 — сообщение игнорирует задокументированный в этом же файле `ERROR_MESSAGE_CAP`**. <details> <summary>📋 F1, F2 — детали</summary> ### F1 [warning · test-coverage] два fail-open гарда `withSpaceAccessDiagnostics` не покрыты — `packages/mcp/src/client/context.ts:878,897` Обе ветки выживают мутацию на чистой сборке (сьют остаётся 11/11): 1. **non-404 passthrough** (`:878` `if(!(isAxiosError && status===404)) throw e`): заменить условие на `false` (500/403/network пошёл бы в `/spaces`-sweep + возможный rewrite) — не краснеет НИЧЕГО: ни один тест не подаёт non-404 ошибку в обёрнутый вызов С непустым spaceId. Это ровно регрессия, которую PR НЕ должен внести (проглотить/переформатировать реальный 500/403) — и она не заперта. 2. **id-present edge** (`:897` `if(idx.ids.has(spaceId)) throw e`): убрать → не краснеет ничего, т.к. все enrich-тесты используют BAD (никогда не в SPACES), `has()` всегда false. Ветка, гасящая ложное «не найден среди доступных», когда спейс НА САМОМ ДЕЛЕ доступен, не прогоняется. **Fix:** 2 теста в `space-access-diagnostics.test.mjs`: (a) `getTree(BAD)`, где enumerate кидает non-404 AxiosError (status 500) при непустом spaceId → ассертить, что оригинальный 500 проходит без rewrite и `c._spacesFetches===0`; (b) `getTree(SPACES[0].id)`, где enumerate кидает `make404()` а индекс СОДЕРЖИТ этот id → ассертить, что оригинальная 404 проходит (не `/не найден среди/`). ### F2 [suggestion · conventions] сообщение не соблюдает `ERROR_MESSAGE_CAP` — `packages/mcp/src/client/errors.ts:64-81` Файл документирует и enforce'ит `ERROR_MESSAGE_CAP=300` (`errors.ts:16,193-194`) с явной целью «model context stays compact и whitelisted серверная строка не раздует текст» — применяет в `formatDocmostAxiosError`. Но `formatSpaceNotAccessible` интерполирует до 10 (`SPACE_LIST_CAP`) серверных `name` без лимита по ДЛИНЕ → ~10 длинноимённых спейсов дают строку в несколько раз больше 300ch. Единственное новое model-facing сообщение, игнорирующее собственный инвариант файла. Блэст-радиус мал (имена — свои же доступные спейсы, не атакер-инпут), потому suggestion. **Fix:** применить кап к собранной строке перед возвратом (переиспользовать slice-and-ellipsis из `formatDocmostAxiosError`, либо усечь каждое имя). <sub>9 аспектов (свёрнуты в 3 агента + мой разбор крюкса). Гейт: mcp build чист, node:test 843/843 + новый 11/11 (мутация rewrite→3 краснеют). База develop, own delta 6 файлов/+661. Дерево после мутаций чисто. ⛔ кодеру НЕ делать: коммент `read.ts:190` (парентеза о том, какой эндпоинт отдаёт 404) неточен, но функционально безвреден — below-threshold, не трогай.</sub> </details>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-12 05:43:12 +03:00
agent_coder added 3 commits 2026-07-12 05:58:19 +03:00
Review follow-up (#536).

F1 (test coverage): add two tests that pin the wrapper's fail-open guards, which
previously survived mutation:
- a non-404 error (500) on a wrapped call with a spaceId propagates unchanged and
  triggers NO /spaces sweep (a real 5xx/403 must never be swallowed/reformatted);
- a 404 when the spaceId IS in the accessible index fails open (the 404 is about
  another resource), so it is not falsely rewritten to "not found among spaces".
Both mutation-verified: forcing the non-404 condition to false reddens the first;
removing the id-present guard reddens the second.

F2 (conventions): formatSpaceNotAccessible now honours ERROR_MESSAGE_CAP (300),
the same budget formatDocmostAxiosError enforces. Only the interpolated space
list is truncated (with an ellipsis); the fixed prefix (bad spaceId) and suffix
(listSpaces pointer) are always kept, so the actionable parts survive. Test: 10
long space names -> message <= 300 and still contains the spaceId + listSpaces.
Adjusted the existing list-cap test to short ids/names so it exercises the 10-item
cap + "(+N ещё)" tail below the length cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The list-only cap assumed a well-formed prefix; a pathologically long
agent-supplied spaceId (unvalidated for length) lives in the prefix and bypassed
the cap. Add the same unconditional final slice formatDocmostAxiosError uses, so
the WHOLE message is bounded regardless of spaceId length. No-op for normal
inputs (36-char UUID) where the list cap already keeps it <= 300.

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

Обе находки закрыты (46bb55db + мерж develop). Внутренний цикл: 1 фикс-проход, оба guard'а подтверждены хирургичными мутациями.

F1: fixed — два fail-open guard'а withSpaceAccessDiagnostics покрыты. Добавил 2 теста в space-access-diagnostics.test.mjs: (a) non-404 passthrough — getTree(BAD), enumerate кидает 500 при непустом spaceId → ассерт: оригинальная 500 проходит без rewrite И _spacesFetches===0 (никакого /spaces-свипа — та самая регрессия, которую PR не должен внести); (b) id-present edge — getTree(SPACES[0].id), id ЕСТЬ в индексе, wrapped-вызов кидает 404 → ассерт: оригинальная 404 проходит (не «не найден среди»), _spacesFetches===1. Мутации хирургичны: status===404||true → краснеет РОВНО non-404 тест; снять if(idx.ids.has(spaceId)) → краснеет РОВНО id-present тест.
F2: fixedformatSpaceNotAccessible теперь соблюдает ERROR_MESSAGE_CAP=300: список спейсов режется по бюджету CAP - prefix - suffix (prefix с spaceId и suffix с listSpaces всегда целы), 10 длинных имён → ≤300 с spaceId+listSpaces. Плюс (по ре-ревью) добавил безусловный финальный backstop-срез как в formatDocmostAxiosError — на патологически длинный агент-переданный spaceId (не валидируется по длине, живёт в prefix): без backstop'а он обошёл бы cap (5000-символьный spaceId → 5117), теперь всё сообщение ≤300 при любом входе.

📋 Проверка

space-access-diagnostics.test.mjs 14/14; полный mcp unit 704, mock 142. Backstop проверен: spaceId 5000 символов → сообщение ровно 300; нормальный вход (36-символьный UUID + 10 спейсов) → 300, spaceId и listSpaces целы. context.ts (сам механизм enrich) НЕ тронут — F1 test-only. node_modules 0. Не трогал коммент read.ts:190 (below-threshold). Влил develop.

Обе находки закрыты (`46bb55db` + мерж develop). Внутренний цикл: 1 фикс-проход, оба guard'а подтверждены хирургичными мутациями. `F1: fixed` — два fail-open guard'а `withSpaceAccessDiagnostics` покрыты. Добавил 2 теста в `space-access-diagnostics.test.mjs`: (a) non-404 passthrough — `getTree(BAD)`, enumerate кидает 500 при непустом spaceId → ассерт: оригинальная 500 проходит без rewrite И `_spacesFetches===0` (никакого /spaces-свипа — та самая регрессия, которую PR не должен внести); (b) id-present edge — `getTree(SPACES[0].id)`, id ЕСТЬ в индексе, wrapped-вызов кидает 404 → ассерт: оригинальная 404 проходит (не «не найден среди»), `_spacesFetches===1`. Мутации хирургичны: `status===404||true` → краснеет РОВНО non-404 тест; снять `if(idx.ids.has(spaceId))` → краснеет РОВНО id-present тест. `F2: fixed` — `formatSpaceNotAccessible` теперь соблюдает `ERROR_MESSAGE_CAP=300`: список спейсов режется по бюджету `CAP - prefix - suffix` (prefix с spaceId и suffix с listSpaces всегда целы), 10 длинных имён → ≤300 с spaceId+listSpaces. **Плюс** (по ре-ревью) добавил безусловный финальный backstop-срез как в `formatDocmostAxiosError` — на патологически длинный агент-переданный spaceId (не валидируется по длине, живёт в prefix): без backstop'а он обошёл бы cap (5000-символьный spaceId → 5117), теперь всё сообщение ≤300 при любом входе. <details> <summary>📋 Проверка</summary> `space-access-diagnostics.test.mjs` **14/14**; полный mcp unit **704**, mock **142**. Backstop проверен: spaceId 5000 символов → сообщение ровно 300; нормальный вход (36-символьный UUID + 10 спейсов) → 300, spaceId и listSpaces целы. `context.ts` (сам механизм enrich) НЕ тронут — F1 test-only. node_modules 0. Не трогал коммент `read.ts:190` (below-threshold). Влил develop. </details>
agent_coder added review/needs and removed review/changes-requested labels 2026-07-12 05:58:42 +03:00
Collaborator

Ре-ревью — MCP enrich-404 (#534), раунд 2. Вердикт: approved

Обе находки закрыты, проверил адверсариально:

  • F1 [fixed] два fail-open гарда теперь покрыты + замучены мутацией (сам прогнал на собранном модуле): (a) тест «non-404 (500) propagates unchanged, NO /spaces sweep» (space-access-diagnostics.test.mjs:172) — мутация if(false)throw e на non-404-гарде → тест краснеет; (b) тест «404 when spaceId IS in the accessible index fails open» (:194) — мутация if(false) на id-present-гарде → тест краснеет. Оба не вакуумны, стерегут ровно те ветки.
  • F2 [fixed] formatSpaceNotAccessible теперь применяет ERROR_MESSAGE_CAP (budget = CAP − prefix − suffix, усечение listed + финальный кап всего сообщения, errors.ts:80-99). Эмпирика: 10 длинноимённых спейсов → сообщение ровно 300 (≤300 ✓, хвост усечён); короткое (1 спейс) → 135, имя цело.

Гейт: mcp node:test 880/880 (было 843 + новые тесты). Дерево после мутаций чисто. Готово к мержу.

## Ре-ревью — MCP enrich-404 (#534), раунд 2. Вердикт: **approved** ✅ Обе находки закрыты, проверил адверсариально: - **F1 [fixed]** два fail-open гарда теперь покрыты + **замучены мутацией** (сам прогнал на собранном модуле): (a) тест «non-404 (500) propagates unchanged, NO /spaces sweep» (`space-access-diagnostics.test.mjs:172`) — мутация `if(false)throw e` на non-404-гарде → тест краснеет; (b) тест «404 when spaceId IS in the accessible index fails open» (`:194`) — мутация `if(false)` на id-present-гарде → тест краснеет. Оба не вакуумны, стерегут ровно те ветки. - **F2 [fixed]** `formatSpaceNotAccessible` теперь применяет `ERROR_MESSAGE_CAP` (budget = CAP − prefix − suffix, усечение `listed` + финальный кап всего сообщения, `errors.ts:80-99`). **Эмпирика:** 10 длинноимённых спейсов → сообщение ровно 300 (≤300 ✓, хвост усечён); короткое (1 спейс) → 135, имя цело. **Гейт:** mcp node:test **880/880** (было 843 + новые тесты). Дерево после мутаций чисто. Готово к мержу.
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 06:15:21 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/534-mcp-spaceid:fix/534-mcp-spaceid
git checkout fix/534-mcp-spaceid
Sign in to join this conversation.