Merge pull request 'лендинг: имя боевого сервера убрано из пользовательской копии + гейт' (#3273) from fix/mera-drop-prod-hostname into main
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled

This commit is contained in:
bot-backend 2026-08-30 10:12:55 +00:00
commit 70df446d09
3 changed files with 93 additions and 3 deletions

View file

@ -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([]);
});
});

View file

@ -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)
* не подтвердил ни одно:
*
* | макет | что показал замер |

View file

@ -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 дней, дедуп по источнику и адресу), " +
"собственный адрес из когорты исключён";