From b1bc6fd6e7fcf074162d3f24215bb06f6aa30eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 8 Aug 2026 23:28:40 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=B6=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B0=D0=B7=D0=B2=D0=B8=D0=BB=D0=BA=D1=83:=20?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D1=91=D1=82=20=D0=BE=D0=B1=20=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B5=20=D0=B8=D0=B4=D1=91?= =?UTF-8?q?=D1=82=20=D0=B2=20=D1=81=D0=B2=D0=BE=D0=B9=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C,=20=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B2=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Связка «задание вида stop → TelegramOstanovkaApplier» не охранялась ничем: удали или перепутай ветку match — и отчёт уехал бы в применитель ЗАПУСКА кампании, который двигает статус и деньги. Опасность в этом файле известна и записана над самим match, но для нового вида работы сторожа ещё не было. Сторож ходит через настоящую ручку робота, а не зовёт применитель напрямую: отказ по заданию stop обязан снять отметку «останавливается» — этого не делает никто, кроме нужного применителя. Co-Authored-By: Claude Opus 5 (1M context) --- .../Feature/ClientTg/TgRobotDoneModesTest.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/tests/Feature/ClientTg/TgRobotDoneModesTest.php b/app/tests/Feature/ClientTg/TgRobotDoneModesTest.php index 1a9e1d10..37f1f461 100644 --- a/app/tests/Feature/ClientTg/TgRobotDoneModesTest.php +++ b/app/tests/Feature/ClientTg/TgRobotDoneModesTest.php @@ -94,3 +94,42 @@ it('отказ пересдачи при наличии номера кабин expect(Campaign::find($campaign->id)->status)->toBe(Campaign::STATUS_NEEDS_REVIEW); }); + +// 🔴 Развилка не охранялась ничем: удали/перепутай ветку MODE_STOP в match — и отчёт об +// остановке уехал бы в default (применитель ЗАПУСКА), который двигает статус и деньги. +// Отказ по заданию stop обязан снять отметку «останавливается» — этого не делает никто, +// кроме TelegramOstanovkaApplier, так что отметка — самый наглядный различитель. +it('отчёт об отказе остановки снимает отметку, а не двигает кампанию как после запуска', function () { + $campaign = tgModesCampaign($this->tenant->id, Campaign::STATUS_LAUNCHED); + $campaign->update(['stop_requested_at' => now()]); + $job = $this->queue->enqueue($this->tenant->id, $campaign->id, RobotJob::MODE_STOP, ['campaignId' => '777']); + $this->queue->takeNext(); + + $this->withHeader('X-Tg-Robot-Token', 'tg-secret-1') + ->postJson("/api/tg-robot/jobs/{$job->id}/done", [ + 'ok' => false, 'step' => 'stop', 'reason' => 'Вход в кабинет слетел', + ]) + ->assertOk(); + + $fresh = Campaign::find($campaign->id); + expect($fresh->stop_requested_at)->toBeNull(); + expect($fresh->status)->toBe(Campaign::STATUS_LAUNCHED); +}); + +it('успешное нажатие «Завершить» оставляет отметку остановки и записывает цифры', function () { + $campaign = tgModesCampaign($this->tenant->id, Campaign::STATUS_LAUNCHED); + $campaign->update(['stop_requested_at' => now()]); + $job = $this->queue->enqueue($this->tenant->id, $campaign->id, RobotJob::MODE_STOP, ['campaignId' => '777']); + $this->queue->takeNext(); + + $this->withHeader('X-Tg-Robot-Token', 'tg-secret-1') + ->postJson("/api/tg-robot/jobs/{$job->id}/done", [ + 'ok' => true, 'stopped' => true, 'status' => 'stopping', 'impressions' => 22, 'spendRub' => '10.56', + ]) + ->assertOk(); + + $fresh = Campaign::find($campaign->id); + expect($fresh->stop_requested_at)->not->toBeNull(); + expect($fresh->status)->toBe(Campaign::STATUS_LAUNCHED); + expect((string) $fresh->stats_spend_rub)->toBe('10.56'); +});