press-releases: Mail "wird persoenlich geprueft" beim Block, Blade-Guard fuer @php-Bloecke (Roadmap 1d, Nachtrag)
Der Autor bekommt beim Block nach dem letzten Rot (oder Gelb mit eingeschalteter Queue) eine kurze Mail statt Stillstand: was passiert ist, dass nichts zu tun ist, und dass das Ergebnis per Mail kommt. Laeuft ueber notifyAuthor() wie Veroeffentlichung und Ablehnung (API-Regel, Sperrliste, adressbezogene Abmeldung, Bounce-Referenz); Opt-out am Typ "abgelehnt". Neue ConversionSource mail_review fuer den Link ins Konto. BladePhpBlockGuardTest wendet das Compiler-Muster auf alle Views an und faengt inline @php( vor einem spaeteren @endphp ab — fand den eigenen Erklaer-Kommentar im Modal, der nur zufaellig funktionierte. Regel in CLAUDE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9380937562
commit
4dffb1368f
11 changed files with 278 additions and 24 deletions
42
tests/Feature/BladePhpBlockGuardTest.php
Normal file
42
tests/Feature/BladePhpBlockGuardTest.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Blade liest `@php … @endphp` VOR allen anderen Direktiven und Kommentaren
|
||||
* mit dem Muster `/(?<!@)@php(.*?)@endphp/s` (BladeCompiler::storePhpBlocks).
|
||||
* Ein inline `@php(...)` vor einem späteren Block wird dadurch samt allem
|
||||
* dazwischen als ein Rohblock genommen — die Seite kompiliert nicht mehr
|
||||
* („unexpected end of file, expecting endif") oder verliert Inhalt. Das
|
||||
* gilt auch für `@php(`, das nur in einem Blade-Kommentar steht.
|
||||
*
|
||||
* Regel: Sobald eine Datei `@endphp` enthält, gibt es darin kein inline
|
||||
* `@php(` — immer die Blockform benutzen.
|
||||
*/
|
||||
test('no view mixes inline @php( with a later @php block', function () {
|
||||
$offenders = [];
|
||||
|
||||
foreach (File::allFiles(resource_path('views')) as $view) {
|
||||
if (! str_ends_with($view->getFilename(), '.blade.php')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contents = File::get($view->getRealPath());
|
||||
|
||||
if (! str_contains($contents, '@endphp')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Exakt das Muster des Compilers: jeder Treffer, dessen Inhalt mit
|
||||
// `(` beginnt, ist ein inline @php(, das einen Block verschluckt.
|
||||
preg_match_all('/(?<!@)@php(.*?)@endphp/s', $contents, $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches[1] as [$body, $offset]) {
|
||||
if (str_starts_with($body, '(')) {
|
||||
$offenders[] = sprintf('%s:%d', $view->getRelativePathname(), substr_count($contents, "\n", 0, $offset) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect($offenders)->toBe([], "Inline @php( vor einem @php-Block — auf Blockform umstellen:\n".implode("\n", $offenders));
|
||||
});
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\EmailNotificationType;
|
||||
use App\Enums\PressReleaseClassification;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Jobs\ClassifyPressRelease;
|
||||
use App\Mail\PressReleaseHeldForReview;
|
||||
use App\Mail\PressReleaseRejected;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseStatusLog;
|
||||
|
|
@ -70,8 +72,41 @@ test('each release counts its automatic checks and the last red blocks instead o
|
|||
->and(PressReleaseStatusLog::query()->where('press_release_id', $pr->id)->latest('id')->first()->reason)
|
||||
->toContain('Automatische Prüfung erschöpft (2 von 2)');
|
||||
|
||||
// Keine Ablehnungsmail für den Block — der Autor sieht den Stand im Konto.
|
||||
// Keine Ablehnungsmail für den Block — stattdessen „ein Mensch prüft jetzt".
|
||||
Mail::assertNotQueued(PressReleaseRejected::class, fn ($mail) => str_contains((string) $mail->reason, 'erschöpft'));
|
||||
Mail::assertQueued(PressReleaseHeldForReview::class, function (PressReleaseHeldForReview $mail) use ($customer, $pr) {
|
||||
return $mail->hasTo($customer->email)
|
||||
&& $mail->automationExhausted === true
|
||||
&& $mail->pressRelease->is($pr)
|
||||
&& str_contains($mail->showUrl, '/admin/me/press-releases/'.$pr->id.'?src=mail_review&pr='.$pr->id);
|
||||
});
|
||||
});
|
||||
|
||||
test('the held-for-review mail says nothing is required and respects the rejected opt-out', function () {
|
||||
/** @var TestCase $this */
|
||||
[$customer, $pr] = gateDraft();
|
||||
$pr->update(['title' => 'Zurückgehaltene Meldung']);
|
||||
|
||||
$html = (new PressReleaseHeldForReview($customer, $pr, automationExhausted: true))->render();
|
||||
|
||||
expect($html)
|
||||
->toContain('Zurückgehaltene Meldung')
|
||||
->toContain('auch im 2. Durchlauf nicht')
|
||||
->toContain('Sie müssen nichts tun')
|
||||
->toContain('Stand der Prüfung ansehen');
|
||||
$this->assertStringNotContainsString('Ablehnungsgrund', $html);
|
||||
|
||||
$yellow = (new PressReleaseHeldForReview($customer, $pr, automationExhausted: false))->render();
|
||||
expect($yellow)->toContain('zur Sicherheit zurückgehalten');
|
||||
$this->assertStringNotContainsString('Durchlauf', $yellow);
|
||||
|
||||
// Wer die Ablehnungsmails abbestellt hat, bekommt auch den Zwischenstand nicht.
|
||||
$customer->optOutOfEmailNotification(EmailNotificationType::PressReleaseRejected);
|
||||
$pr->update(['auto_check_attempts' => 2, 'status' => PressReleaseStatus::Review->value]);
|
||||
redRun($pr, 'K1: Rot');
|
||||
|
||||
expect($pr->fresh()->isAwaitingManualReview())->toBeTrue();
|
||||
Mail::assertNotQueued(PressReleaseHeldForReview::class);
|
||||
});
|
||||
|
||||
test('after the human decision the automation stays off for this release', function () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue