9cc3f5e950
Хвосты денег Д1-Д6 и робота Р-х1-Р-х6 из приёмочного листа v12. Д1 цена, по которой заморожены деньги, записывается на кампанию. Пока поле было пустым, списание читало глобальную цену — админ менял её, и клиент платил больше обещанного при запуске. Д2 суточное списание берёт кампанию под замком строки. Ключ идемпотентности зависит от числа показов, поэтому два одновременных прогона получали разные ключи и списали бы клиента дважды. Д3 пауза, не дошедшая до Директа, больше не считается паузой: отказ 409, заморозка остаётся. Раньше реклама крутилась дальше, портал показывал паузу, а деньги были уже свободны. У возобновления поведение намеренно прежнее — иначе понадобилось бы пятое место разморозки, а их ровно четыре. Там же убрана мина строгого сравнения рубильника. Д4 не трогали — это вопрос владельца. Д5 рубильник Директа держит и служебный канал робота: выдача задания и приём отчёта ходили в живой кабинет мимо него. Д6 проверка рубильника приведена к общему виду: YANDEX_DIRECT_ENABLED=0 давало строку, которую строгое сравнение читало как включено. Р-х1 настройки читаются из .env робота, а не каталога запуска. Р-х2 файл-замок robot.lock: проход и поддержание входа больше не дерутся за профиль браузера. Занят — уходим молча, задание остаётся в очереди. Брошенный замок перехватывается через полчаса. Р-х3 письмо-алярм честно говорит, залиты ли уже креативы в кабинет. Побочно вскрылось, что тексты писем не проверялись ни одним тестом — транспорт вынесен в src/smtp.js. Р-х4 тест-пустышка про рабочую папку заменён настоящим: запуск из чужого каталога без явной папки. Проверено вырезанием. Р-х6 пустое значение в окружении читается как значение по умолчанию, мусор даёт внятную ошибку вместо тихого NaN. Портал 293/293, робот 57/57. Денежных выходов снятия заморозки по-прежнему четыре. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
4.3 KiB
JavaScript
75 lines
4.3 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { loadConfig } from '../src/config.js';
|
||
|
||
const full = {
|
||
YC_BROWSER_PROFILE_DIR: '/var/lib/liderra-robot/profile',
|
||
YC_CABINET_URL: 'https://direct.yandex.ru',
|
||
PORTAL_BASE_URL: 'https://lk.liderra.ru',
|
||
CREATIVE_ROBOT_TOKEN: 'secret',
|
||
SMTP_HOST: 'smtp.example', SMTP_PORT: '587', SMTP_USER: 'u', SMTP_PASS: 'p',
|
||
ALARM_FROM: 'bot@liderra.ru', ALARM_TO: 'ops@liderra.ru',
|
||
YC_CABINET_LOGIN: 'nash-login',
|
||
YC_ANTEROOM_CAMPAIGN_ID: '713051718',
|
||
YC_ANTEROOM_GROUP_ID: '5776749172',
|
||
};
|
||
|
||
test('загружает полный конфиг', () => {
|
||
const c = loadConfig(full);
|
||
assert.equal(c.portalBaseUrl, 'https://lk.liderra.ru');
|
||
assert.equal(c.robotToken, 'secret');
|
||
assert.equal(c.humanDelayMs, 800);
|
||
});
|
||
|
||
test('знает паузу между повторами доклада порталу', () => {
|
||
assert.equal(loadConfig(full).reportRetryDelayMs, 3000);
|
||
assert.equal(loadConfig({ ...full, REPORT_RETRY_DELAY_MS: '500' }).reportRetryDelayMs, 500);
|
||
});
|
||
|
||
// 🪤 `Number(env.X ?? '800')` ловит только undefined. Пустое значение в .env
|
||
// (`HUMAN_DELAY_MS=`) даёт пустую строку, а Number('') — это 0: робот начинал бы щёлкать
|
||
// по кабинету с машинной скоростью вместо человекоподобного темпа, и именно так выглядит
|
||
// поведение бота для антифрода Яндекса.
|
||
test('пустое значение паузы в окружении читается как значение по умолчанию, а не как ноль', () => {
|
||
assert.equal(loadConfig({ ...full, HUMAN_DELAY_MS: '' }).humanDelayMs, 800);
|
||
assert.equal(loadConfig({ ...full, REPORT_RETRY_DELAY_MS: '' }).reportRetryDelayMs, 3000);
|
||
});
|
||
|
||
// Мусор в значении молча превращался в NaN: пауза становилась «никакой», а порт почты —
|
||
// NaN, и письма переставали уходить без единого внятного слова в журнале. Пустое значение
|
||
// и опечатка — разные вещи: пустое значит «не задавал, возьми обычное», опечатка значит
|
||
// «человек хотел что-то задать и ошибся». Про второе надо сказать вслух, а не подставлять
|
||
// умолчание молча.
|
||
test('мусор в значении паузы падает с понятным сообщением', () => {
|
||
assert.throws(() => loadConfig({ ...full, HUMAN_DELAY_MS: 'быстро' }), /HUMAN_DELAY_MS/);
|
||
});
|
||
|
||
test('нечисловой порт почты падает с понятным сообщением, а не превращается в NaN', () => {
|
||
assert.throws(() => loadConfig({ ...full, SMTP_PORT: 'пятьсот' }), /SMTP_PORT/);
|
||
});
|
||
|
||
test('падает с понятным сообщением, если нет обязательной переменной', () => {
|
||
const { CREATIVE_ROBOT_TOKEN, ...without } = full;
|
||
assert.throws(() => loadConfig(without), /CREATIVE_ROBOT_TOKEN/);
|
||
});
|
||
|
||
test('срезает хвостовой слеш у адреса портала, чтобы не собирать двойные слеши', () => {
|
||
assert.equal(loadConfig({ ...full, PORTAL_BASE_URL: 'https://lk.liderra.ru/' }).portalBaseUrl, 'https://lk.liderra.ru');
|
||
});
|
||
|
||
test('срезает хвостовой слеш и у адреса кабинета', () => {
|
||
assert.equal(loadConfig({ ...full, YC_CABINET_URL: 'https://direct.yandex.ru/' }).cabinetUrl, 'https://direct.yandex.ru');
|
||
});
|
||
|
||
test('знает логин кабинета и «прихожую» — кампанию с группой, форму которой открывает робот', () => {
|
||
const c = loadConfig(full);
|
||
assert.equal(c.cabinetLogin, 'nash-login');
|
||
assert.equal(c.anteroomCampaignId, '713051718');
|
||
assert.equal(c.anteroomGroupId, '5776749172');
|
||
});
|
||
|
||
test('требует «прихожую»: без неё роботу негде открыть окно загрузки', () => {
|
||
const { YC_ANTEROOM_GROUP_ID, ...without } = full;
|
||
assert.throws(() => loadConfig(without), /YC_ANTEROOM_GROUP_ID/);
|
||
});
|