e724311e38
Теперь на опрос переведены все три работы робота, а не только запуск кампании. Портал кладёт задание в таблицу, робот сам приходит за ним и отчитывается. Это нужно потому, что робот живёт не на машине очереди - МТС не пускает адреса дата-центров. Что сделано по плану docs/superpowers/plans/2026-07-31-tg-poll-read-status-resubmit.md: - два новых режима задания: чтение вердикта модерации и пересдача; - применение вердикта вынуто из джоба в TelegramModerationVerdictApplier, применение итога пересдачи - в TelegramResubmitResultApplier; оба переноса построчные, денежная логика не менялась ни в одном символе; - у обоих джобов появилась развилка по каналу: на опросе задание ставится, робот не запускается; - приёмщик отчёта различает режимы - иначе отчёт о чтении вердикта применился бы как отчёт о запуске и сдвинул кампанию не туда; - номера телефонов больше не выдаются режимам, которым они не нужны: чтению вердикта и пересдаче аудитория не требуется, она у кампании уже есть; - у робота развилка по режиму вынесена в отдельный src/poll-plan.js, чтобы её можно было проверять без браузера и без сети; - сторож прав на бою: роль портала обязана иметь право ставить задания. Новых миграций и новых прав НЕ понадобилось: все три места ставят задание под подключением по умолчанию, то есть под ролью портала, у которой права уже есть. Схема БД не менялась, запись в CHANGELOG не требуется. Приёмка вырезанием: убираем ограничение по режиму в выдаче номеров - два теста падают, возвращаем - зелёные. Проверено: телеграм на портале 244 из 244 (было 219, старые тесты в том числе) робот 124 из 124 (было 120) статанализ 0 настоящих замечаний полный прогон 4039 тестов, 3995 прошло, 20 упало Из 20 падений 19 - те же давние, что были до работы. Двадцатое - ExternalServiceDownAlertTest, в одиночку проходит 3 из 3 и вместе с телеграм- тестами тоже; падает только в полном прогоне от накопленных данных. Это известная слабость: у большинства файлов нет изоляции между тестами. Заодно сборка тестовой БД переведена с migrate:fresh на связку db:wipe --drop-types + migrate: первая спотыкалась на призрачном типе legal_entities, вторая на тех же состояниях отрабатывала без отказов. На бой не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
55 lines
2.6 KiB
PHP
55 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
it('таблица заданий роботу существует с нужными столбцами', function () {
|
|
expect(Schema::hasTable('client_tg_robot_jobs'))->toBeTrue();
|
|
|
|
foreach ([
|
|
'id', 'tenant_id', 'campaign_id', 'mode', 'payload', 'status',
|
|
'attempts', 'result', 'failure_reason', 'taken_at', 'finished_at',
|
|
] as $column) {
|
|
expect(Schema::hasColumn('client_tg_robot_jobs', $column))->toBeTrue("нет столбца {$column}");
|
|
}
|
|
});
|
|
|
|
it('на таблице включена построчная защита с политикой по тенанту', function () {
|
|
$rls = DB::selectOne("SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'client_tg_robot_jobs'");
|
|
expect($rls->relrowsecurity)->toBeTrue();
|
|
expect($rls->relforcerowsecurity)->toBeTrue();
|
|
|
|
$policy = DB::selectOne("SELECT polname FROM pg_policy p JOIN pg_class c ON c.oid = p.polrelid WHERE c.relname = 'client_tg_robot_jobs'");
|
|
expect($policy->polname)->toBe('tenant_isolation');
|
|
});
|
|
|
|
/**
|
|
* Мина, которую на dev не видно: там ходит суперпользователь, а на бою — роли.
|
|
*
|
|
* Задания ставят ТРИ места (запуск кампании, опросчик вердикта, пересдача), и все три
|
|
* работают под подключением по умолчанию, то есть под ролью портала. Служебной роли
|
|
* crm_supplier_worker права на эту таблицу НЕ нужны: она используется только для
|
|
* кросс-тенантного ПЕРЕЧИСЛЕНИЯ кампаний в чужой таблице.
|
|
*/
|
|
it('роль портала имеет права ставить и вести задания, если роли вообще заведены', function () {
|
|
$roleExists = DB::selectOne("SELECT 1 AS x FROM pg_roles WHERE rolname = 'crm_app_user'");
|
|
|
|
if ($roleExists === null) {
|
|
expect(true)->toBeTrue(); // dev: ролей нет, ходит суперпользователь — проверять нечего
|
|
|
|
return;
|
|
}
|
|
|
|
$grants = DB::select(
|
|
"SELECT privilege_type FROM information_schema.role_table_grants
|
|
WHERE table_name = 'client_tg_robot_jobs' AND grantee = 'crm_app_user'"
|
|
);
|
|
$types = array_map(fn ($g) => $g->privilege_type, $grants);
|
|
|
|
expect($types)->toContain('INSERT');
|
|
expect($types)->toContain('SELECT');
|
|
expect($types)->toContain('UPDATE');
|
|
});
|