From c5ca3c4387a2b9b44d772c0e2ac3139580d61a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 2 Aug 2026 17:07:07 +0300 Subject: [PATCH] =?UTF-8?q?fix,=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0:=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=B5=D1=82=20=D0=BD=D0=B0=20=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=D0=BE=D0=B4=20=D0=B2=20=D1=81=D0=B5=D1=82=D1=8C=20?= =?UTF-8?q?=E2=80=94=20=D1=82=D0=B5=D1=87=D1=8C=20=D0=B1=D1=8B=D0=BB=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=B2=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=BC=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B5,=20=D0=B0=20=D0=B2=2093?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Промт называл один тест, который печатал сетевую ошибку. Сторож нашёл 93 теста в 14 файлах: остальные 92 глушились чужими catch внутри рабочего кода и были невидимы вообще. Запрета на выход наружу в тестах интерфейса не было ни одного за всю историю — тот же класс, что закрыли 01.08 на PHP. Сторож в app/tests/Frontend/setup.ts делает две вещи, и обе нужны: обрывает запрос и запоминает адрес, роняя тест в afterEach. Без второй половины код, который глушит отказ своим catch, снова всё прячет. Там же поимённо заглушены четыре фоновые точки оболочки — счётчик сделок, состояние автоподбора, статус баланса, отметка о посещении. Их не спрашивает ни один тест разметки, а отказ у всех четырёх и так проглатывается. Остальные семь файлов глушат свои вызовы у себя; общий ответ статусов воронки лежит в tests/Frontend/support/fixtures.ts. Заглушки повторяют то состояние, которое тесты видели при отказе сети, поэтому ни одна проверка не переписывалась. track.spec.ts снимает общую заглушку через vi.unmock — он проверяет сам заглушенный код. Приёмка: сторож проверен красным трижды — обычный запрос, запрос с проглоченной ошибкой, fetch. Полный набор интерфейса 233 файла / 1750 тестов / 3 пропущено / 0 падений / 0 ошибок. Линтер интерфейса 0, проверка типов 5 — те же унаследованные, что были до смены. Рабочий код не тронут. Co-Authored-By: Claude Opus 5 (1M context) --- .../AutopodborManualCompetitorError.spec.ts | 5 + app/tests/Frontend/ChangePasswordCard.spec.ts | 10 +- .../Frontend/DealsListIntegration.spec.ts | 8 ++ app/tests/Frontend/DealsView.spec.ts | 8 ++ .../FieldCompetitorScreenCreate.spec.ts | 8 ++ app/tests/Frontend/KanbanRedesign.spec.ts | 10 +- app/tests/Frontend/KanbanView.spec.ts | 8 ++ app/tests/Frontend/SettingsView.spec.ts | 21 ++- app/tests/Frontend/setup.ts | 120 ++++++++++++++++++ app/tests/Frontend/support/fixtures.ts | 19 +++ app/tests/Frontend/track.spec.ts | 5 + .../2026-08-02-PROMT-podgotovka-k-produ-8.md | 46 ++++++- 12 files changed, 259 insertions(+), 9 deletions(-) diff --git a/app/tests/Frontend/AutopodborManualCompetitorError.spec.ts b/app/tests/Frontend/AutopodborManualCompetitorError.spec.ts index 276a1839..110405ec 100644 --- a/app/tests/Frontend/AutopodborManualCompetitorError.spec.ts +++ b/app/tests/Frontend/AutopodborManualCompetitorError.spec.ts @@ -41,6 +41,11 @@ describe('FieldWorkspaceScreen — «Добавить вручную» объя vi.spyOn(store, 'loadField').mockImplementation(async () => { store.field = [] as never; }); + // Экран при открытии тянет ДВА списка сразу (см. reload() в FieldWorkspaceScreen). + // Без этой заглушки тест физически слал запрос наружу на /api/autopodbor/proposals + // и всё равно был зелёным — ошибка вылетала мимо проверок. Ловит сторож сети + // в tests/Frontend/setup.ts. + vi.spyOn(store, 'loadProposalGroups').mockResolvedValue(undefined); vi.spyOn(store, 'addFieldCompetitor').mockRejectedValue({ isAxiosError: true, // как настоящая ошибка axios response: { diff --git a/app/tests/Frontend/ChangePasswordCard.spec.ts b/app/tests/Frontend/ChangePasswordCard.spec.ts index f30b608d..1bcd810a 100644 --- a/app/tests/Frontend/ChangePasswordCard.spec.ts +++ b/app/tests/Frontend/ChangePasswordCard.spec.ts @@ -1,4 +1,12 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; + +// Карточка на открытии спрашивает дату последней смены пароля. Без заглушки тест +// физически ходил в сеть на /api/account/security и всё равно был зелёным: отказ +// глушился своим catch. Отдаём «пароль не менялся» — ровно то, что видел тест раньше. +vi.mock('../../resources/js/api/account', async (importOriginal) => ({ + ...(await importOriginal()), + getAccountSecurity: async () => ({ last_password_change_at: null, sessions: [] }), +})); import { mount } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; diff --git a/app/tests/Frontend/DealsListIntegration.spec.ts b/app/tests/Frontend/DealsListIntegration.spec.ts index b183a8ca..ea375804 100644 --- a/app/tests/Frontend/DealsListIntegration.spec.ts +++ b/app/tests/Frontend/DealsListIntegration.spec.ts @@ -1,4 +1,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Экраны тянут статусы воронки с сервера. Без заглушки тест физически ходил в сеть на +// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и +// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts. +vi.mock('../../resources/js/api/leadStatuses', async () => { + const { otvetStatusovVoronki } = await import('./support/fixtures'); + return { listLeadStatuses: async () => otvetStatusovVoronki() }; +}); import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createRouter, createMemoryHistory } from 'vue-router'; diff --git a/app/tests/Frontend/DealsView.spec.ts b/app/tests/Frontend/DealsView.spec.ts index 45bf40d6..bda048b9 100644 --- a/app/tests/Frontend/DealsView.spec.ts +++ b/app/tests/Frontend/DealsView.spec.ts @@ -1,4 +1,12 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; + +// Экран тянет статусы воронки с сервера. Без заглушки тест физически ходил в сеть на +// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и +// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts. +vi.mock('../../resources/js/api/leadStatuses', async () => { + const { otvetStatusovVoronki } = await import('./support/fixtures'); + return { listLeadStatuses: async () => otvetStatusovVoronki() }; +}); import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createRouter, createMemoryHistory } from 'vue-router'; diff --git a/app/tests/Frontend/FieldCompetitorScreenCreate.spec.ts b/app/tests/Frontend/FieldCompetitorScreenCreate.spec.ts index 96247027..9b34cd81 100644 --- a/app/tests/Frontend/FieldCompetitorScreenCreate.spec.ts +++ b/app/tests/Frontend/FieldCompetitorScreenCreate.spec.ts @@ -1,4 +1,12 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Окно «реквизиты» на открытии читает уже сохранённые. Без заглушки тест физически +// ходил в сеть на /api/tenant/requisites и всё равно был зелёным: экран нарочно +// fail-open и глушит отказ. `null` = пустая форма — ровно то, что видел тест раньше. +vi.mock('../../resources/js/api/requisites', async (importOriginal) => ({ + ...(await importOriginal()), + getRequisites: async () => null, +})); import { mount } from '@vue/test-utils'; import { setActivePinia, createPinia } from 'pinia'; import { AxiosError } from 'axios'; diff --git a/app/tests/Frontend/KanbanRedesign.spec.ts b/app/tests/Frontend/KanbanRedesign.spec.ts index 527bd4d6..cd86ebaa 100644 --- a/app/tests/Frontend/KanbanRedesign.spec.ts +++ b/app/tests/Frontend/KanbanRedesign.spec.ts @@ -1,4 +1,12 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Экран тянет статусы воронки с сервера. Без заглушки тест физически ходил в сеть на +// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и +// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts. +vi.mock('../../resources/js/api/leadStatuses', async () => { + const { otvetStatusovVoronki } = await import('./support/fixtures'); + return { listLeadStatuses: async () => otvetStatusovVoronki() }; +}); import { mount, flushPromises } from '@vue/test-utils'; import { createPinia, setActivePinia } from 'pinia'; import { createVuetify } from 'vuetify'; diff --git a/app/tests/Frontend/KanbanView.spec.ts b/app/tests/Frontend/KanbanView.spec.ts index 06541ffa..cef4efdf 100644 --- a/app/tests/Frontend/KanbanView.spec.ts +++ b/app/tests/Frontend/KanbanView.spec.ts @@ -1,4 +1,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Экран тянет статусы воронки с сервера. Без заглушки тест физически ходил в сеть на +// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и +// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts. +vi.mock('../../resources/js/api/leadStatuses', async () => { + const { otvetStatusovVoronki } = await import('./support/fixtures'); + return { listLeadStatuses: async () => otvetStatusovVoronki() }; +}); import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createPinia, setActivePinia } from 'pinia'; diff --git a/app/tests/Frontend/SettingsView.spec.ts b/app/tests/Frontend/SettingsView.spec.ts index 6f806920..f36921d1 100644 --- a/app/tests/Frontend/SettingsView.spec.ts +++ b/app/tests/Frontend/SettingsView.spec.ts @@ -1,4 +1,23 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; + +// Вкладки настроек на открытии тянут свои данные. Без заглушек тест физически ходил +// в сеть на /api/account/security, /api/api-keys и /api/tenants/me/webhook-settings — +// и всё равно был зелёным: отказы глушились своими catch. Отдаём «пусто», ровно то +// состояние, которое тест видел раньше. Ловит сторож сети в tests/Frontend/setup.ts. +vi.mock('../../resources/js/api/account', async (importOriginal) => ({ + ...(await importOriginal()), + getAccountSecurity: async () => ({ last_password_change_at: null, sessions: [] }), +})); + +vi.mock('../../resources/js/api/apiKeys', async (importOriginal) => ({ + ...(await importOriginal()), + listApiKeys: async () => [], +})); + +vi.mock('../../resources/js/api/webhooks', async (importOriginal) => ({ + ...(await importOriginal()), + getWebhookSettings: async () => null, +})); import { mount } from '@vue/test-utils'; import { createPinia } from 'pinia'; import { createVuetify } from 'vuetify'; diff --git a/app/tests/Frontend/setup.ts b/app/tests/Frontend/setup.ts index f4996665..cc45f8fc 100644 --- a/app/tests/Frontend/setup.ts +++ b/app/tests/Frontend/setup.ts @@ -1,6 +1,10 @@ // Vitest setup для Vuetify в JSDOM-среде. // Vuetify-компоненты используют ResizeObserver/IntersectionObserver/matchMedia, // которые отсутствуют в JSDOM. Добавляем минимальные stub'ы. +// +// 🔴 Плюс запрет на выход в сеть — см. блок «Сторож сети» ниже. + +import { afterEach, vi } from 'vitest'; class ResizeObserverStub { observe(): void {} @@ -49,6 +53,122 @@ if (!window.CSS || !window.CSS.supports) { }); } +// ───────────────────────────────────────────────────────────────────────────── +// Сторож сети: тест не имеет права ходить наружу по-настоящему. +// +// 02.08.2026. До этого дня запрета не было ни одного, и тест +// AutopodborManualCompetitorError.spec.ts физически слал XHR на +// /api/autopodbor/proposals — оставаясь при этом ЗЕЛЁНЫМ, потому что ошибка +// вылетала мимо проверок. Тот же класс разбирали 01.08 на PHP-тестах. +// +// Сторож делает две вещи, и обе нужны: +// 1) обрывает запрос — наружу ничего не уходит; +// 2) ЗАПОМИНАЕТ адрес и роняет тест в afterEach — иначе код под тестом может +// проглотить ошибку своим catch, и нарушение снова станет невидимым. +// +// Правильное лечение в тесте — заглушить вызов (vi.mock на модуль api/* +// или vi.spyOn на метод хранилища), а не отключать сторожа. +// ───────────────────────────────────────────────────────────────────────────── + +const setevyeVylazki: string[] = []; + +function zapomnitVylazku(adres: string): Error { + setevyeVylazki.push(adres); + + return new Error( + `Тест пытается сходить в сеть по-настоящему: ${adres}. ` + + 'Заглушите вызов (vi.mock модуля api/* или vi.spyOn на методе хранилища).', + ); +} + +const rodnoyOpen = window.XMLHttpRequest.prototype.open; + +type XhrSAdresom = XMLHttpRequest & { __adresVylazki?: string }; + +window.XMLHttpRequest.prototype.open = function ( + this: XhrSAdresom, + method: string, + url: string | URL, + ...ostalnoe: unknown[] +): void { + this.__adresVylazki = `${method} ${String(url)}`; + + // rodnoyOpen перегружен, точный тип аргументов здесь не выразить — + // передаём как есть. + (rodnoyOpen as (...a: unknown[]) => void).call(this, method, url, ...ostalnoe); +}; + +window.XMLHttpRequest.prototype.send = function (this: XhrSAdresom): void { + throw zapomnitVylazku(this.__adresVylazki ?? 'адрес неизвестен'); +}; + +globalThis.fetch = ((vhod: unknown): never => { + const adres = + typeof vhod === 'string' + ? vhod + : vhod instanceof URL + ? vhod.href + : ((vhod as { url?: string })?.url ?? 'адрес неизвестен'); + + throw zapomnitVylazku(`fetch ${adres}`); +}) as unknown as typeof globalThis.fetch; + +afterEach(() => { + if (setevyeVylazki.length === 0) return; + + const spisok = [...new Set(setevyeVylazki)].join(', '); + setevyeVylazki.length = 0; + + throw new Error( + `Тест ходил в сеть по-настоящему: ${spisok}. ` + + 'Ошибку мог проглотить catch внутри кода — поэтому она поймана здесь, после теста.', + ); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Фоновые вызовы оболочки — заглушены по умолчанию для ВСЕХ тестов. +// +// Боковое меню (AppSidebar) и шапка (AppLayout) при открытии сами тянут три вещи: +// счётчик сделок на бейдже, состояние автоподбора и статус баланса. Плюс роутер +// шлёт отметку о посещении. Ни один тест разметки про них не спрашивает, и все три +// вызова в рабочем коде обёрнуты в catch — то есть их отказ НЕВИДИМ. +// Именно поэтому 92 теста в 13 файлах годами ходили в сеть по-настоящему и никто +// об этом не знал (замер 02.08.2026). +// +// 🔑 Это НЕ отключение сторожа: заглушены ровно четыре фоновые точки, названные +// поимённо. Любой другой запрос сторож выше по-прежнему ловит и роняет тест. +// Тест, которому нужно другое поведение этих четырёх, объявляет свой vi.mock — +// он побеждает этот. +// ───────────────────────────────────────────────────────────────────────────── + +vi.mock('../../resources/js/api/deals', async (importOriginal) => ({ + ...(await importOriginal()), + fetchDealsCount: async (): Promise => 0, +})); + +vi.mock('../../resources/js/api/autopodbor', async (importOriginal) => ({ + ...(await importOriginal()), + fetchState: async () => ({ enabled: false, runs: [], prices: { search: '0.00', study: '0.00' } }), +})); + +vi.mock('../../resources/js/api/billing', async (importOriginal) => ({ + ...(await importOriginal()), + getBalanceStatus: async () => ({ + frozen_by_balance_at: null, + balance_rub: '0.00', + capacity_leads: 0, + required_leads_per_day: 0, + deficit_leads: 0, + deficit_rub: '0.00', + }), +})); + +vi.mock('../../resources/js/plugins/track', () => ({ + trackVisit: () => {}, + trackScreen: () => {}, + trackRegisterOpen: () => {}, +})); + // visualViewport — VOverlay/v-menu/v-snackbar используют для location strategies. if (!window.visualViewport) { Object.defineProperty(window, 'visualViewport', { diff --git a/app/tests/Frontend/support/fixtures.ts b/app/tests/Frontend/support/fixtures.ts index 95f88a9e..af922d49 100644 --- a/app/tests/Frontend/support/fixtures.ts +++ b/app/tests/Frontend/support/fixtures.ts @@ -1,5 +1,7 @@ import type { CompetitorDto, FieldCompetitorDto, FieldSourceDto, RunDto, SourceProjectDto } from '../../../resources/js/api/autopodbor'; +import type { ApiLeadStatus } from '../../../resources/js/api/leadStatuses'; import type { AutopodborCtx, AutopodborNav, ScreenName } from '../../../resources/js/views/autopodbor/nav'; +import { LEAD_STATUSES } from '../../../resources/js/composables/leadStatuses'; import { reactive, ref } from 'vue'; import { vi } from 'vitest'; @@ -114,6 +116,23 @@ export function navCtx(over: Partial = {}): AutopodborCtx { }); } +/** + * Ответ `GET /api/lead-statuses` — ровно те же пять системных статусов, что лежат + * снимком в коде (`LEAD_STATUSES`). Экраны «Сделки» и «Канбан» тянут их на открытии; + * до 02.08.2026 запрос уходил в сеть по-настоящему, падал, и хранилище молча + * оставалось на снимке. Заглушка отдаёт то же самое, только честно. + */ +export function otvetStatusovVoronki(): ApiLeadStatus[] { + return LEAD_STATUSES.map((s) => ({ + slug: s.slug, + name_ru: s.nameRu, + is_system: s.isSystem, + sort_order: s.sortOrder, + color_hex: s.colorHex, + description: null, + })); +} + /** Навигация-заглушка: `go` — шпион, состояние полное. */ export function navStub(screen: ScreenName = 'entry', ctxOver: Partial = {}): AutopodborNav { return { go: vi.fn(), ctx: navCtx(ctxOver), screen: ref(screen) }; diff --git a/app/tests/Frontend/track.spec.ts b/app/tests/Frontend/track.spec.ts index eaa449b2..63521674 100644 --- a/app/tests/Frontend/track.spec.ts +++ b/app/tests/Frontend/track.spec.ts @@ -1,5 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; +// Этот файл проверяет САМ счётчик посещений, поэтому общая заглушка из setup.ts +// здесь снимается — иначе тест проверял бы заглушку вместо рабочего кода. +// Выход в сеть тут закрыт своей подменой `fetch` ниже. +vi.unmock('../../resources/js/plugins/track'); + describe('свой учёт посетителей', () => { beforeEach(() => { vi.resetModules(); diff --git a/docs/superpowers/2026-08-02-PROMT-podgotovka-k-produ-8.md b/docs/superpowers/2026-08-02-PROMT-podgotovka-k-produ-8.md index 68ad0280..0d26197e 100644 --- a/docs/superpowers/2026-08-02-PROMT-podgotovka-k-produ-8.md +++ b/docs/superpowers/2026-08-02-PROMT-podgotovka-k-produ-8.md @@ -106,13 +106,38 @@ PSR_v1, Tooling, CLAUDE.md). Файл `Tooling_v8_3.md` огромный, пра два экрана (`FieldCompetitorScreen.vue`, `NewProjectDialog.vue`) и три теста рекламы. Записи не мешают — хук проверку типов не гоняет. -### Е. 🌐 Тест ходит в настоящую сеть +### Е. ✅ ЗАКРЫТО 02.08.2026 — тесты интерфейса ходили в настоящую сеть -В полном прогоне тестов интерфейса печатается сетевая ошибка `ERR_NETWORK` на -`/api/autopodbor/proposals` из `tests/Frontend/AutopodborManualCompetitorError.spec.ts`. -Тест при этом зелёный, потому что ошибка вылетает мимо проверок. Это **тот же класс**, что -разбирали 01.08 на PHP-тестах: тест физически ходит наружу. Здесь адрес относительный, так -что до интернета не доходит, — но запрос не заглушен, и это стоит закрыть. +**Переделывать не надо.** + +🔴 **Тест был не один.** В промте стоял один файл — тот, у которого ошибка случайно +вылезла наружу и напечаталась. Замер сторожем показал **93 теста в 14 файлах**: +остальные 92 глушились чужими `catch` внутри рабочего кода и были невидимы вообще. +Запрета на выход наружу в тестах интерфейса **не было ни одного за всю историю** — +ровно как на PHP до 01.08. + +**Что сделано.** В `app/tests/Frontend/setup.ts` появился сторож сети: подменяет +`XMLHttpRequest.send` и `fetch`, обрывает запрос **и** запоминает адрес, а в `afterEach` +роняет тест. Вторая половина обязательна: без неё код под тестом проглатывает ошибку +своим `catch`, и нарушение снова становится невидимым. Там же заглушены **четыре +фоновые точки оболочки**, названные поимённо (счётчик сделок, состояние автоподбора, +статус баланса, отметка о посещении) — их не спрашивает ни один тест разметки, а +отказ у всех четырёх и так проглатывается. Остальные семь файлов заглушают свои +вызовы у себя; общий ответ `GET /api/lead-statuses` лежит в +`tests/Frontend/support/fixtures.ts` (`otvetStatusovVoronki`). + +🪤 **Заглушка в `setup.ts` действует на ВСЕ файлы — включая тот, что проверяет сам +заглушенный код.** `track.spec.ts` начал проверять заглушку вместо счётчика посещений. +Лечение — `vi.unmock` в самом файле, одной строкой и с объяснением. + +🔑 Заглушка обязана повторять **то состояние, которое тест видел раньше** (при отказе +сети), иначе чинишь одно и ломаешь другое: пустые реквизиты, «пароль не менялся», +те же пять статусов воронки. + +Приёмка: сторож проверен красным трижды (обычный запрос, запрос с проглоченной +ошибкой, `fetch`). Полный набор интерфейса — **233 файла / 1750 тестов / 3 пропущено / +0 падений / 0 ошибок**. Линтер интерфейса 0, проверка типов **5** — те же +унаследованные, что были до смены. ### Ж. Индекс памяти @@ -175,6 +200,9 @@ PSR_v1, Tooling, CLAUDE.md). Файл `Tooling_v8_3.md` огромный, пра - **Долг изоляции тестов** — после всей `tests/Feature` в базе не остаётся ничего лишнего. Сторож — `tools/storozh-chistoty-bazy.mjs`. +- **Выход тестов интерфейса в сеть** — закрыт сторожем в `app/tests/Frontend/setup.ts` + (§3 Е). Новый тест, который «почему-то не видит данных», сперва проверь на этого + сторожа: он называет адрес прямо в тексте падения. - **Ошибка суток по всему порталу** — правило названо в `App\Support\MskBoundary`. - **Показ времени приведён к Москве** — держит `tools/storozh-moskovskogo-vremeni.mjs`. @@ -205,6 +233,12 @@ PSR_v1, Tooling, CLAUDE.md). Файл `Tooling_v8_3.md` огромный, пра - 🔴🔑 **Всё, что промт говорит о БОЕВОМ состоянии, — гипотеза, пока не замерена командой.** 02.08 промт назвал два указателя на бою и велел снести не тот; на бою он был ОДИН, и снос убил бы единственный. Промт пишется по памяти, боевое живёт своей жизнью. +- 🔑 **«Один случай» в промте — тоже не замер.** 02.08 промт называл ОДИН тест, ходящий + в сеть; сторож нашёл 93 в 14 файлах. Видно было один — тот, чью ошибку никто не + проглотил. **Считай не то, что видно, а то, что поймал датчик.** +- 🪤 **Заглушка, поставленная на всех сразу, ломает того, кто проверяет её же предмет.** + Общая заглушка в `setup.ts` заставила `track.spec.ts` проверять заглушку вместо кода. + Датчик — полный прогон; лечение — `vi.unmock` в одном файле с объяснением. - 🔑 **Приёмка вырезанием обязательна**, обе половины: подложил поломку — остановил; убрал — пропустил. Зелёный прогон без вырезания не доказывает ничего. - 🪤 **Вырезание доказывает что-то, только если вырезаны ВСЕ пути к результату.** 02.08 у