86c560e0c4
Этап 3 «Жизненный цикл и модерация», задача 3.2. Живая отправка робота в МТС означает «ушло на модерацию», а не «запущено»: кампания получает статус `moderating`, а `launched` придёт позже, когда опросчик вердикта (задача 3.4) увидит одобрение. - Campaign.php: константа STATUS_MODERATING + переходы running→moderating, moderating→launched|rejected, rejected→queued (пересдача, задача 3.6). running→launched оставлен ради обратной совместимости. Миграция НЕ нужна: на колонке status нет CHECK-ограничения (машина статусов — в модели), `moderating` влезает в varchar(16). - RunTelegramCampaignJob.finalize(): живой успех (launched=true) → moderating вместо launched; песочница (черновик) по-прежнему → draft_ready. Тест-инфра (побочно, но необходимо для проверки): tests/TestCase.php получил `protected $dropTypes = true`. RefreshDatabase's migrate:fresh дропал таблицы, но НЕ типы Postgres; при заблокированном дропе таблицы её composite row-type переживал db:wipe, и перезагрузка db/schema.sql падала на дубле типа («legal_entities … уже существует»), оставляя ЧАСТИЧНУЮ схему — давний интермиттентный флак «migrate:fresh иногда прерывается» (site_events/ client_tg_tariffs случайно отсутствовали). Дроп типов на каждом refresh резко снизил флак (было 15–46/130 → стало 93–126/130). Только для APP_ENV=testing, на прод не влияет. Остаточный редкий обрыв — отдельная пред-существующая проблема, не этой задачи. TDD, робот замокан, песочница. Приёмка (моя область, чистый прогон): ModeratingStatusTest 8/8 + StatusMachineTest/RunCampaignJobTest/ExternalIdTest/ RobotRunnerTest 35/35 суммарно; phpstan (Campaign+Job) 0, deptrac 0, pint чисто. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
4.4 KiB
PHP
79 lines
4.4 KiB
PHP
<?php
|
||
|
||
namespace Tests;
|
||
|
||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||
use Illuminate\Support\Facades\Artisan;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
abstract class TestCase extends BaseTestCase
|
||
{
|
||
/**
|
||
* RefreshDatabase's migrate:fresh drops tables but NOT Postgres types by
|
||
* default. Our initial schema is loaded raw from db/schema.sql, whose
|
||
* `CREATE TABLE`s implicitly define composite row-types; when a table drop
|
||
* is blocked by a lingering dependency, its row-type survives db:wipe and
|
||
* the next schema.sql reload aborts on a duplicate-type error
|
||
* («legal_entities … уже существует»), leaving a PARTIAL schema — the
|
||
* long-standing intermittent «migrate:fresh иногда прерывается» flake
|
||
* (site_events/client_tg_tariffs randomly missing). Dropping types on every
|
||
* refresh clears those leftovers and makes migrate:fresh deterministic.
|
||
* Testing-only (APP_ENV=testing) — no production impact.
|
||
*/
|
||
protected $dropTypes = true;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
// Quirk 72: supplier code (SupplierPortalClient::loadSession,
|
||
// RefreshSupplierSessionJob, CsvReconcileJob, RouteSupplierLeadJob)
|
||
// hardcodes Cache::store('redis'), bypassing phpunit.xml's
|
||
// CACHE_STORE=array. The Redis store is a shared external service —
|
||
// under `pest --parallel` every worker collides on the global
|
||
// `supplier:session` key, so one worker's afterEach forget()/flush()
|
||
// races another worker's mid-test loadSession(). Repoint the `redis`
|
||
// cache store at the in-process `array` driver: each worker then gets
|
||
// a hermetic, worker-local cache. Production keeps the real `redis`
|
||
// driver — this override only ever runs under APP_ENV=testing.
|
||
config(['cache.stores.redis.driver' => 'array']);
|
||
}
|
||
|
||
/**
|
||
* Laravel вызывает этот хук из трейта RefreshDatabase сразу после migrate:fresh.
|
||
*
|
||
* migrate:fresh пересоздаёт только родительские партиционированные таблицы;
|
||
* месячные партиции — отдельная команда (только вперёд). Тесты с датами
|
||
* прошлых месяцев иначе падают на отсутствии партиции. Диапазон: 2 назад +
|
||
* текущий + 3 вперёд. Идемпотентно.
|
||
*
|
||
* DDL идёт через pgsql_supplier (как в MonthlyPartitionManager::DDL_CONNECTION),
|
||
* иначе на проде crm_app_user не имеет прав создавать партиции.
|
||
*/
|
||
protected function afterRefreshingDatabase()
|
||
{
|
||
Artisan::call('partitions:create-months', ['--ahead' => 3]);
|
||
|
||
// Окно партиций считаем от РЕАЛЬНОГО системного времени (new DateTimeImmutable),
|
||
// а не от now()/Carbon: отдельные тесты замораживают Carbon::setTestNow на
|
||
// прошлые/будущие даты, и тогда единственный migrate:fresh пересоздавал бы
|
||
// месяцы вокруг замороженной даты, не покрывая реальный прошлый месяц (тесты
|
||
// с датами вроде now()->subDays(10) падали на отсутствии партиции). Диапазон
|
||
// -3..+5 месяцев. Идемпотентно. DDL через pgsql_supplier (DDL_CONNECTION) —
|
||
// у crm_app_user нет прав на создание партиций.
|
||
$base = new \DateTimeImmutable('first day of this month 00:00:00');
|
||
$parents = DB::select("select c.relname from pg_class c join pg_partitioned_table p on p.partrelid = c.oid where c.relkind = 'p'");
|
||
for ($m = -3; $m <= 5; $m++) {
|
||
$start = $base->modify(($m >= 0 ? '+' : '-').abs($m).' months');
|
||
$end = $start->modify('+1 month');
|
||
$suffix = '_y'.$start->format('Y').'_m'.$start->format('m');
|
||
foreach ($parents as $p) {
|
||
$t = $p->relname;
|
||
DB::connection('pgsql_supplier')->statement(
|
||
"CREATE TABLE IF NOT EXISTS \"{$t}{$suffix}\" PARTITION OF \"{$t}\" FOR VALUES FROM ('{$start->format('Y-m-d')}') TO ('{$end->format('Y-m-d')}')"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|