From 2bffb178e3a50202b1f2f2a745e2838402356b8f Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 20 Aug 2026 11:20:24 +0500 Subject: [PATCH] =?UTF-8?q?test(tradein):=20=D0=B3=D0=B5=D0=B9=D1=82=20?= =?UTF-8?q?=C2=AB=D1=81=D0=BD=D1=8F=D1=82=D1=8C=20noindex=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B5=20=D1=81=20=D0=BF=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=D1=81=D1=85=D0=BE=D0=BB=D0=B4=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=C2=BB=20(#2904)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Задача владельца требует: до снятия robots:{index:false} каждая маркетинговая цифра лэндинга либо заменяется вычисляемой, либо снимается со страницы — публикация недостоверных характеристик платной услуги подпадает под ст. 5 ФЗ «О рекламе» и ЗоЗПП. Модуль констант marketing-v3.ts уже существует и несёт в шапке таблицу «заявлено против замера»: ошибка 1,8% против 13,2% медианы |%| (бэктест n=277) 83% в диапазон против 81,95% при коридоре шириной 74% от точки ±6 дн по сроку против «сверки срока с фактом НЕ СУЩЕСТВУЕТ» 42 700 проверок за год против 1 084 оценок всего (база живёт 2,5 месяца) 42 города РФ против 6 городов области, полное покрытие — 1 Но САМ ГЕЙТ жил только в комментарии. Правило, записанное словами, не мешает снять одну строку в layout.tsx и выкатить страницу: комментарий не участвует в сборке. Здесь он становится механическим — тест краснеет ровно в момент, когда noindex исчезает, а плейсхолдеры остаются на странице. Тест НЕ запрещает открывать лэндинг. Он требует, чтобы открытие шло вместе с заменой чисел, то есть чтобы это было решением, а не побочным следствием правки метаданных. Когда плейсхолдеры уйдут из компонентов, гейт сам перестанет срабатывать (ранний возврат при пустом списке). Проверено фальсификацией, а не рассуждением: * снял robots:{index:false} → тест краснеет и перечисляет, какие именно величины ещё на странице; * сломал зонд (переименовал пути компонентов) → краснеет ВТОРОЙ тест («ни один плейсхолдер не найден — зонд сломан»). Без него первый проходил бы вхолостую: пустой список плейсхолдеров означает «всё заменено». Оба исходника восстановлены, дерево чистое. Третья проверка держит в шапке модуля таблицу расхождений: потеряется она — и следующий читатель решит, что цифры подтверждены замером. vitest (tradein-mvp/frontend): 11 файлов, 59 тестов, rc=0 type-check: rc=0 (локальный прогон ронял устаревший .next/types со ссылкой на удалённую страницу scrapers/cian-cookies — артефакт старой сборки, в CI чекаут чистый; проверено отдельно) --- .../marketing-placeholders-gate.test.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts new file mode 100644 index 00000000..8f6ed5d1 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts @@ -0,0 +1,105 @@ +/** + * Гейт #2904: снять `noindex` можно только вместе с плейсхолдерными числами. + * + * В `marketing-v3.ts` лежат величины из дизайн-макета, НЕ подтверждённые замером: + * + * | здесь | факт по замеру | + * | ошибка 1,8% | 13,2% медиана |%| (бэктест n=277), held-out 16,1% | + * | 83% в диапазон | 81,95%, но коридор шириной 74% от точки | + * | ±6 дн по сроку | сверки срока с фактом НЕ СУЩЕСТВУЕТ | + * | 42 700 проверок за год | 1 084 оценки всего (база живёт 2,5 месяца) | + * | 42 города РФ | 6 городов области, полное покрытие — 1 (ЕКБ) | + * + * Пока страница закрыта `robots: { index: false }`, эти строки никому не видны, и + * держать их допустимо. Публикация же недостоверных характеристик ПЛАТНОЙ услуги + * подпадает под ст. 5 ФЗ «О рекламе» и ЗоЗПП. + * + * До сих пор это правило жило ТОЛЬКО в комментарии модуля. Правило, записанное + * словами, не мешает снять одну строку в layout.tsx и выкатить страницу — поэтому + * гейт здесь механический: он краснеет ровно в тот момент, когда noindex исчезает, + * а плейсхолдеры остаются. + * + * Тест НЕ запрещает открывать страницу. Он требует, чтобы открытие шло вместе с + * заменой чисел на вычисляемые (или их снятием) — то есть чтобы решение было + * осознанным, а не побочным следствием правки метаданных. + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const ROOT = join(__dirname, ".."); + +function read(rel: string): string { + return readFileSync(join(ROOT, rel), "utf8"); +} + +/** Компоненты лэндинга v3, которые могут ссылаться на плейсхолдеры. */ +const V3_SOURCES = [ + "v3/page.tsx", + "_components/v3/HeroV3.tsx", + "_components/v3/AccuracyV3.tsx", + "_components/v3/FreeCheckCard.tsx", + "_components/v3/FreeResultV3.tsx", +]; + +const PLACEHOLDER_EXPORTS = [ + "ACCURACY_PLACEHOLDER", + "COST_OF_ERROR_PLACEHOLDER", + "FREE_SAMPLE_PLACEHOLDER", + "PROOF_ROWS_PLACEHOLDER", +]; + +function placeholdersInUse(): string[] { + const used: string[] = []; + for (const rel of V3_SOURCES) { + let src: string; + try { + src = read(rel); + } catch { + continue; // файл переименован/удалён — не повод падать + } + for (const name of PLACEHOLDER_EXPORTS) { + if (src.includes(name) && !used.includes(name)) used.push(name); + } + } + return used; +} + +function noindexIsOn(): boolean { + const layout = read("layout.tsx").replace(/\s+/g, " "); + return layout.includes("robots: { index: false"); +} + +describe("гейт noindex ↔ плейсхолдерные числа (#2904)", () => { + it("пока плейсхолдеры на странице — noindex обязан стоять", () => { + const used = placeholdersInUse(); + if (used.length === 0) { + // Числа заменены вычисляемыми — гейт больше не нужен, открывать можно. + return; + } + expect(noindexIsOn(), [ + `лэндинг v3 всё ещё использует непроверенные величины: ${used.join(", ")},`, + "но `robots: { index: false }` в layout.tsx снят.", + "Перед открытием страницы наружу каждая цифра обязана быть заменена", + "вычисляемой либо снята — см. шапку marketing-v3.ts и issue #2904.", + ].join(" ")).toBe(true); + }); + + it("сам гейт видит и плейсхолдеры, и noindex — иначе он проходил бы вхолостую", () => { + // Контроль на инструмент: если оба зонда перестанут находить своё + // (переименование файлов, смена формы записи), проверка выше станет + // зелёной по построению и перестанет что-либо охранять. + expect(placeholdersInUse().length, "ни один плейсхолдер не найден в компонентах v3 — зонд сломан").toBeGreaterThan(0); + expect(read("layout.tsx"), "layout.tsx не содержит блока robots — зонд сломан").toContain("robots"); + }); + + it("расхождение «заявлено против замера» остаётся в шапке модуля", () => { + // Таблица — единственное место, где зафиксировано, НАСКОЛЬКО числа расходятся + // с фактом. Потеряется она — и следующий читатель решит, что цифры проверены. + const mod = read("marketing-v3.ts"); + expect(mod).toContain("13,2%"); + expect(mod).toContain("НЕ СУЩЕСТВУЕТ"); + expect(mod).toContain("1 084"); + }); +}); -- 2.45.3