test(tradein): гейт «снять noindex можно только вместе с плейсхолдерами» (#2904)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 52s

Задача владельца требует: до снятия 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 чекаут
чистый; проверено отдельно)
This commit is contained in:
bot-backend 2026-08-20 11:20:24 +05:00
parent 251eacc4b6
commit 2bffb178e3

View file

@ -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");
});
});