Files
portal/bots/mts-telegram-ads/test/task.test.js
T
Дмитрий 18135b47b4 feat(телеграм-реклама): Этап 3.5/3.6 + 4.3 — чтение вердикта, пересдача, уведомление об одобрении
Закрывает Этап 3 «Жизненный цикл и модерация» целиком (3.1–3.6) и бэкенд-часть 4.3.
План docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.

## 3.5 — робот читает статус/причину модерации по mts_campaign_id (Node)
- `parseModerationStatus` (cabinet.js) — чистый парсер статус-текста кабинета в канон
  Laravel-опросчика: «Отклонена»→rejected, «Одобрена»/«Активна»→approved,
  «На модерации»→moderating, «Черновик»→draft, иначе null (тогда опросчик ждёт).
  Терминальные вердикты приоритетнее слова «модераци…» в строке-блобе. Юнит-тест
  read-status.test.mjs (11 кейсов: регистр/nbsp/блоб/приоритет/мусор).
- `readModerationStatus` (cabinet.js) + `runReadStatus` (runner.js) + режим `read-status`
  в bin/run.js: открывает список кампаний, находит ряд по id, читает статус; при отказе —
  причину из слайд-модалки «Причины» (#slide-modal-root). Отдаёт JSON
  {ok, moderationStatus, reason?, campaignId} — его уже разбирает RobotResult (3.4).
  🔴 Читалка кабинета помечена <FLOW-CONFIRM>: DOM-обёртки ряда/модалки собраны по
  живой разведке «Сессии 6» (FLOW-FINDINGS.md), но именно этим кодом live ещё не прогнаны —
  подтвердить на следующем цикле модерации с разрешения владельца. Парсер от DOM не зависит.

## 3.6 — пересдача отклонённой кампании (rejected → queued + документ модератору)
- Миграция 000013: колонка `client_tg_campaigns.moderator_file_path` (varchar 500 NULL,
  после media_path) + CHANGELOG схемы v8.90; rls-reviewer прогнан — чисто (nullable-колонка
  данных, не tenant-скоуп, RLS не меняется). Модель — fillable.
- Endpoint `POST /api/telegram/campaigns/{id}/resubmit`: только отклонённую (иначе 422);
  правки ad_text/ad_link/ord_category (валидация как store) + опц. файл модератору
  (.png/.jpeg/.jpg/.pdf ≤10 МБ, сохраняется на диск local). Успех: поля обновлены,
  status_reason и mts_campaign_id очищены (робот создаст новую кампанию в кабинете),
  rejected→queued, dispatch RunTelegramCampaignJob afterCommit. В бою — гейт аудитории
  + freeze budget_cap_rub заново (при отказе бронь вернул опросчик 3.4; freeze идемпотентен
  по ACTIVE-холду), нехватка → 409, остаётся rejected. Песочница — без брони.
- Робот: task `moderatorFile` (task.js passthrough + TelegramRobotRunner.taskPayload),
  RunTelegramCampaignJob отдаёт moderator_file_path; fillAd грузит файл в поле «Комментарий
  для модератора» (третий file-input, accept pdf) — помечено <FLOW-CONFIRM> (live не прогнан).
- Тесты: ResubmitTest.php (7 кейсов: rejected→queued+очистка+джоб / файл сохранён /
  не-rejected→422 / live 409 / валидация / .exe→422 / чужой→404); Node task.test.js (+2).

## 4.3 — уведомление об одобрении (бэкенд был готов в 3.4, добор покрытия)
- ApproveNotifyTest.php (4 кейса): notifyTelegramCampaignApproved шлёт in-app всем активным
  юзерам тенанта без pref-гейта, тело «одобрена/показы пошли»; неактивный/чужой не получают.

TDD. Приёмка (моя область, чистый прогон): весь ClientTg 150/150, робот 78/78,
ApproveNotify 4/4; phpstan 0, deptrac 0, pint чисто.

Приёмочный лист 3.6 — docs/superpowers/2026-07-28-telegram-3.6-resubmit-acceptance.md.
Осталось в Этапе 4: фронтенд 4.1/4.2/4.4/4.5 (Vue-экран + Vitest) — НЕ начато.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 11:29:23 +03:00

61 lines
2.2 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseTask } from '../src/task.js';
const base = {
phonesFile: 'phones.txt', adText: 'Короткий текст',
budgetRub: 1000, buttonUrl: 'https://site.ru',
mode: 'draft',
};
test('parseTask принимает корректное задание', () => {
const t = parseTask(base);
assert.equal(t.mode, 'draft');
assert.equal(t.budgetRub, 1000);
});
test('parseTask отвергает неизвестный режим', () => {
assert.throws(() => parseTask({ ...base, mode: 'xxx' }), /mode/);
});
test('parseTask требует phonesFile', () => {
const { phonesFile, ...rest } = base;
assert.throws(() => parseTask(rest), /phonesFile/);
});
test('parseTask отвергает пустой текст', () => {
assert.throws(() => parseTask({ ...base, adText: '' }), /adText/);
});
test('parseTask требует budgetRub > 0', () => {
assert.throws(() => parseTask({ ...base, budgetRub: 0 }), /budgetRub/);
});
test('parseTask подставляет дефолт ordCategory', () => {
const t = parseTask(base);
assert.equal(t.ordCategory, 'Размещение рекламы');
});
test('parseTask пробрасывает заданную ordCategory', () => {
const t = parseTask({ ...base, ordCategory: 'Другая категория' });
assert.equal(t.ordCategory, 'Другая категория');
});
test('parseTask НЕ требует adTitle/ordCompanyName (полей в кабинете нет)', () => {
// Задание без adTitle/ordCompanyName/cpmRub должно проходить.
const t = parseTask(base);
assert.equal(t.adTitle, undefined);
assert.equal(t.ordCompanyName, undefined);
assert.equal(t.cpmRub, undefined);
});
test('parseTask пробрасывает moderatorFile (документ модератору, пересдача 3.6)', () => {
const t = parseTask({ ...base, moderatorFile: 'C:/tmp/licenziya.pdf' });
assert.equal(t.moderatorFile, 'C:/tmp/licenziya.pdf');
});
test('parseTask: без moderatorFile → null (документ не приложили)', () => {
const t = parseTask(base);
assert.equal(t.moderatorFile, null);
});