Files
portal/bots/yandex-creatives/test/bin-lock.test.js
T
Дмитрий 61ac314d59 fix робот-креативов: замок не снимался после удачного прохода, робот работал раз в полчаса
process.exit стоял внутри try, а снятие замка — в finally. Выход обрывает процесс
немедленно, и до finally дело не доходило никогда. Замок оставался лежать после каждого
удачного прохода, и следующие полчаса и run.js, и keepalive.js молча уходили со словами
«робот уже работает»: задание клиента ждало в очереди, а в журнале при этом всё
выглядело благополучно. Молчаливый сбой того же класса — «успех» без работы.

Ни один из 80 тестов этого не видел: проверялись модуль замка и проход по отдельности,
а дыра была ровно в шве между ними.

Новый тест bin-lock.test.js запускает bin/run.js настоящим процессом ДВАЖДЫ подряд
против портала-обманки: одиночный запуск эту дыру не показывает. Тест сначала упал
на обоих утверждениях, после починки зелёный. Было 80 тестов, стало 82.

Проверено живьём на рендер-виртуалке: после прохода за заданием и после захода
в кабинет замок снят.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:56:35 +03:00

117 lines
5.3 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { createServer } from 'node:http';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
/**
* Шов между запуском и замком: проверяем НЕ модуль замка по отдельности, а сам bin/run.js
* целиком — запуском настоящего процесса.
*
* 🪤 Ради чего написано. Замок снимался в блоке finally, а выход стоял внутри try —
* `process.exit()` обрывает процесс НЕМЕДЛЕННО, и finally не выполняется никогда.
* Замок оставался лежать после каждого удачного прохода, и следующие полчаса робот
* молча уходил со словами «робот уже работает»: задание ждало в очереди, а в журнале
* при этом всё выглядело благополучно. Ни один из 80 тестов этого не видел — все они
* проверяли модуль замка и проход по отдельности, а дыра была ровно в шве между ними.
*
* Отсюда и форма теста: он ДОХОДИТ до состояния «проход закончился», а не собирает его
* руками, и обязательно гоняет проход ДВАЖДЫ подряд — одиночный запуск эту дыру
* не показывает.
*/
const ROBOT_ROOT = fileURLToPath(new URL('..', import.meta.url));
const LOCK_PATH = join(ROBOT_ROOT, 'robot.lock');
const RUN_BIN = join(ROBOT_ROOT, 'bin', 'run.js');
/** Портал-обманка: на вопрос «есть работа?» честно отвечает «нет». Браузер при этом не нужен. */
function startStubPortal() {
return new Promise((resolve) => {
const server = createServer((req, res) => {
if (req.url === '/api/creative-robot/next') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ job: null }));
return;
}
res.writeHead(404);
res.end();
});
server.listen(0, '127.0.0.1', () => resolve({ server, port: server.address().port }));
});
}
function runBin(bin, port, profileDir) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [bin], {
cwd: tmpdir(), // запускаем ИЗ ЧУЖОГО каталога — робота гоняет расписание, не человек
env: {
...process.env,
YC_BROWSER_PROFILE_DIR: profileDir,
YC_CABINET_URL: 'https://direct.yandex.ru',
YC_CABINET_LOGIN: 'proverka',
YC_ANTEROOM_CAMPAIGN_ID: '1',
YC_ANTEROOM_GROUP_ID: '2',
PORTAL_BASE_URL: `http://127.0.0.1:${port}`,
CREATIVE_ROBOT_TOKEN: 'parol-dveri-dlya-proverki',
SMTP_HOST: '127.0.0.1',
SMTP_PORT: '1025',
SMTP_USER: 'proverka',
SMTP_PASS: 'proverka',
ALARM_FROM: 'robot@example.test',
ALARM_TO: 'ops@example.test',
},
});
let out = '';
child.stdout.on('data', (chunk) => { out += chunk; });
child.stderr.on('data', (chunk) => { out += chunk; });
child.on('close', (code) => resolve({ code, out }));
});
}
test('проход снимает замок за собой', async () => {
rmSync(LOCK_PATH, { force: true });
const profileDir = mkdtempSync(join(tmpdir(), 'robot-profil-'));
const { server, port } = await startStubPortal();
try {
const first = await runBin(RUN_BIN, port, profileDir);
assert.equal(first.code, 0, `проход должен закончиться удачей, а вышло: ${first.out}`);
assert.match(first.out, /idle/, `проход должен сказать «работы нет», а сказал: ${first.out}`);
assert.equal(existsSync(LOCK_PATH), false, 'после законченного прохода замок обязан быть снят');
} finally {
server.close();
rmSync(profileDir, { recursive: true, force: true });
rmSync(LOCK_PATH, { force: true });
}
});
test('второй проход подряд не считает робота занятым', async () => {
rmSync(LOCK_PATH, { force: true });
const profileDir = mkdtempSync(join(tmpdir(), 'robot-profil-'));
const { server, port } = await startStubPortal();
try {
await runBin(RUN_BIN, port, profileDir);
const second = await runBin(RUN_BIN, port, profileDir);
assert.doesNotMatch(
second.out,
/уже работает/,
`второй проход подряд не должен считать робота занятым, а сказал: ${second.out}`,
);
assert.match(second.out, /idle/, `второй проход должен сказать «работы нет», а сказал: ${second.out}`);
} finally {
server.close();
rmSync(profileDir, { recursive: true, force: true });
rmSync(LOCK_PATH, { force: true });
}
});