61ac314d59
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>
117 lines
5.3 KiB
JavaScript
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 });
|
|
}
|
|
});
|