diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts new file mode 100644 index 00000000..6b7d71e7 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-copy-no-infra.test.ts @@ -0,0 +1,90 @@ +/** + * Инфраструктурные имена не должны попадать в файлы пользовательской копии. + * + * 30.08.2026 «Симуляция пробы покрытия на боевой базе (poincare, 30.08.2026)» + * доехала до экрана: `CITY_COVERAGE_SOURCE` собирается в `coverage-copy.ts` и + * рендерится в `EstimateFlow.tsx:323`. На первом экране лендинга её не видно — + * она появляется в форме проверки, после выбора города. Поэтому grep по + * отрендеренной главной давал ноль, и дефект выглядел несуществующим. + * + * Отсюда форма гейта: он смотрит в ИСХОДНИК, а не в вывод. Какой именно путь + * рендера сегодня показывает строку, знать не нужно — завтра он будет другим, + * а хостнейм прода посетителю не сообщает ничего ни на одном из них. + * + * Скан идёт по всему тексту файла, включая комментарии: комментарий рядом с + * копией — заготовка следующей строки копии, и «на боевой базе» в нём ничем + * не хуже имени машины. + */ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const ROOT = join(__dirname, ".."); + +/** + * Имена машин, контейнеров и внутренних хостов. Список ЯВНЫЙ, а не эвристика: + * эвристика на «что-то похожее на хостнейм» ловила бы доменные имена в текстах + * про источники данных (domclick.ru, avito.ru), которые тут законны. + * + * `gendsgn.ru` в список НЕ входит намеренно: это публичный домен B2B, на + * который лендинг ссылается кнопкой «Для бизнеса» (`B2B_URL` в content.ts). + * Запрет на него сломал бы рабочую ссылку — граница проходит по «внутреннее + * имя машины» против «адрес, который посетитель и так увидит в браузере». + */ +const BANNED = [ + "poincare", + "beget", + "tradein-postgres", + "tradein-backend", + "tradein-scraper", + "gendesign-postgres", + "gendesign-backend", + "glitchtip", + "forgejo", +]; + +/** Файлы копии: то, из чего собирается видимый посетителю текст. */ +function copySources(dir: string, acc: string[] = []): string[] { + for (const name of readdirSync(dir)) { + if (name === "__tests__" || name === "node_modules") continue; + const full = join(dir, name); + if (statSync(full).isDirectory()) { + copySources(full, acc); + } else if (name.endsWith(".ts") || name.endsWith(".tsx")) { + acc.push(full); + } + } + return acc; +} + +describe("публичная копия не содержит инфраструктурных имён", () => { + const files = copySources(ROOT); + + it("файлы копии вообще найдены — иначе гейт зелёный по построению", () => { + // Без этой проверки сломанный обход каталога дал бы пустой список и + // «нарушений нет» на любом содержимом. + expect(files.length).toBeGreaterThan(20); + expect(files.some((f) => f.endsWith("landing-facts.ts"))).toBe(true); + expect(files.some((f) => f.endsWith("coverage-copy.ts"))).toBe(true); + }); + + it.each(BANNED)("нигде не встречается «%s»", (needle) => { + const hits: string[] = []; + for (const file of files) { + const text = readFileSync(file, "utf8"); + text.split("\n").forEach((line, i) => { + if (line.toLowerCase().includes(needle)) { + hits.push(`${file.slice(ROOT.length + 1)}:${i + 1}`); + } + }); + } + expect( + hits, + `«${needle}» — инфраструктурное имя в файле пользовательской копии. ` + + `Посетителю оно не сообщает ничего: «на боевой базе» несёт весь смысл. ` + + `Строка может не рендериться сегодня и начать рендериться завтра — ` + + `так и вышло с CITY_COVERAGE_SOURCE в EstimateFlow.tsx.`, + ).toEqual([]); + }); +}); diff --git a/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts b/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts index 7f0bfba8..e9b44953 100644 --- a/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts +++ b/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts @@ -4,7 +4,7 @@ * ЧИСЛА ИЗ МАКЕТА СНЯТЫ (29.08.2026). Макет утверждал «по нашим данным, * средний разрыв между первой ценой в объявлении и ценой сделки — 4,1%, а у * провисевших дольше трёх месяцев доходит до 9%», «расхождение с кадастровой - * до 40%» и плитки −4,3% / ×2,4 / 83%. Прод-замер (poincare, 29.08.2026) + * до 40%» и плитки −4,3% / ×2,4 / 83%. Прод-замер (29.08.2026) * не подтвердил ни одно: * * | макет | что показал замер | diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts b/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts index 176dbe4c..e1ee7300 100644 --- a/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts +++ b/tradein-mvp/frontend/src/app/mera-public/landing-facts.ts @@ -44,7 +44,7 @@ export interface MeasuredValue { /** Общий источник трёх величин ниже — один и тот же прогон сверки. */ const BACKTEST_SOURCE = - "Ручная сверка на проде (poincare, 29.08.2026): прогноз МЕРЫ против цены ДКП " + + "Ручная сверка на боевой базе (29.08.2026): прогноз МЕРЫ против цены ДКП " + "Росреестра по Екатеринбургу, сделки II квартала 2026 года"; /** @@ -204,7 +204,7 @@ export interface CityCoverageMeasure { } export const CITY_COVERAGE_SOURCE = - "Симуляция пробы покрытия на боевой базе (poincare, 30.08.2026): случайные адреса " + + "Симуляция пробы покрытия на боевой базе (30.08.2026): случайные адреса " + "активных объявлений, для каждого — когорта самой ручки /coverage (радиус 1 км, то же " + "число комнат, площадь ±15 %, свежесть 14 дней, дедуп по источнику и адресу), " + "собственный адрес из когорты исключён";