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>
96 lines
3.7 KiB
JavaScript
96 lines
3.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync } from 'node:fs';
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { acquireLock } from '../src/lock.js';
|
|
|
|
async function withDir(fn) {
|
|
const dir = await mkdtemp(join(tmpdir(), 'yc-lock-'));
|
|
try {
|
|
return await fn(dir);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test('свободный замок берётся, файл замка появляется на диске', async () => {
|
|
await withDir(async (dir) => {
|
|
const path = join(dir, 'robot.lock');
|
|
|
|
const lock = acquireLock(path);
|
|
|
|
assert.equal(lock.ok, true);
|
|
assert.equal(existsSync(path), true);
|
|
|
|
lock.release();
|
|
assert.equal(existsSync(path), false);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Ради чего всё: Chromium держит папку профиля под замком, и второй процесс просто
|
|
* не стартует. Раньше это падало ВНУТРИ рабочего блока — и задание помечалось сбойным,
|
|
* хотя ничего не сломано: просто расписание запустило проход поверх ещё не закончившегося
|
|
* (README предписывает гонять проход часто, а поддержание входа — раз в ~15 минут).
|
|
*/
|
|
test('занятый замок вторым процессом не берётся', async () => {
|
|
await withDir(async (dir) => {
|
|
const path = join(dir, 'robot.lock');
|
|
const first = acquireLock(path);
|
|
|
|
const second = acquireLock(path);
|
|
|
|
assert.equal(first.ok, true);
|
|
assert.equal(second.ok, false);
|
|
|
|
first.release();
|
|
assert.equal(acquireLock(path).ok, true, 'после освобождения замок снова доступен');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Обратная сторона: процесс убили насмерть (перезагрузка сервера) — снять замок некому,
|
|
* и робот не работал бы уже никогда. Поэтому просроченный замок перехватывается.
|
|
*/
|
|
test('просроченный замок перехватывается', async () => {
|
|
await withDir(async (dir) => {
|
|
const path = join(dir, 'robot.lock');
|
|
await writeFile(path, JSON.stringify({ pid: 999999, at: Date.now() - 60 * 60 * 1000 }));
|
|
|
|
const lock = acquireLock(path, { staleMs: 30 * 60 * 1000 });
|
|
|
|
assert.equal(lock.ok, true);
|
|
});
|
|
});
|
|
|
|
test('свежий замок не перехватывается по сроку', async () => {
|
|
await withDir(async (dir) => {
|
|
const path = join(dir, 'robot.lock');
|
|
await writeFile(path, JSON.stringify({ pid: 999999, at: Date.now() }));
|
|
|
|
assert.equal(acquireLock(path, { staleMs: 30 * 60 * 1000 }).ok, false);
|
|
});
|
|
});
|
|
|
|
// Замок, испорченный до нечитаемого (обрыв записи, кто-то потрогал руками), не должен
|
|
// запирать робота навсегда: непонятное содержимое считаем просроченным.
|
|
test('нечитаемый замок не запирает робота навсегда', async () => {
|
|
await withDir(async (dir) => {
|
|
const path = join(dir, 'robot.lock');
|
|
await writeFile(path, 'мусор, не json');
|
|
|
|
assert.equal(acquireLock(path).ok, true);
|
|
});
|
|
});
|
|
|
|
test('повторное освобождение не падает', async () => {
|
|
await withDir(async (dir) => {
|
|
const lock = acquireLock(join(dir, 'robot.lock'));
|
|
|
|
lock.release();
|
|
lock.release();
|
|
});
|
|
});
|