From 5c43df24cbdcddfc471d2290c8417e4841ebe83a Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 01:08:41 +0200 Subject: [PATCH 1/5] feat: commit a scheduled commit now from the scheduled commits page Co-Authored-By: Claude Opus 5.5 --- .../UpdateScheduledCommitSubmittedAt.php | 9 +- .../ScheduledCommitSubmissionController.php | 25 +++++ ...CreateScheduledCommitSubmissionRequest.php | 25 +++++ .../js/pages/scheduled-commits/index.tsx | 101 ++++++++++++------ routes/web.php | 3 + tests/Feature/ScheduledCommitsTest.php | 46 ++++++++ 6 files changed, 175 insertions(+), 34 deletions(-) create mode 100644 app/Http/Controllers/ScheduledCommitSubmissionController.php create mode 100644 app/Http/Requests/CreateScheduledCommitSubmissionRequest.php diff --git a/app/Actions/UpdateScheduledCommitSubmittedAt.php b/app/Actions/UpdateScheduledCommitSubmittedAt.php index f9820ac56..fa6a2bf51 100644 --- a/app/Actions/UpdateScheduledCommitSubmittedAt.php +++ b/app/Actions/UpdateScheduledCommitSubmittedAt.php @@ -17,15 +17,18 @@ public function __construct(private CreateCommit $createCommit) // } - public function handle(string $id): bool + /** + * The author can commit it now, before its scheduled time. + */ + public function handle(string $id, bool $early = false): bool { - return DB::transaction(function () use ($id): bool { + return DB::transaction(function () use ($id, $early): bool { // Claim with a conditional write before reading. // Creation and the claim roll back together if either operation fails. // Moderation is checked in the claim too, since a timeout or ban can land after the batch was selected... $claimed = ScheduledCommit::query()->whereKey($id) ->whereNull('submitted_at')->whereNull('canceled_at') - ->where('scheduled_at', '<=', now()) + ->unless($early, fn (Builder $query): Builder => $query->where('scheduled_at', '<=', now())) ->whereDoesntHave('user.timeouts', fn (Builder $query): Builder => $query->where('ends_at', '>', now())) ->whereDoesntHave('user.ban') ->update(['submitted_at' => now()]); diff --git a/app/Http/Controllers/ScheduledCommitSubmissionController.php b/app/Http/Controllers/ScheduledCommitSubmissionController.php new file mode 100644 index 000000000..c3fe04832 --- /dev/null +++ b/app/Http/Controllers/ScheduledCommitSubmissionController.php @@ -0,0 +1,25 @@ +handle($scheduledCommit->id, early: true); + + Inertia::flash('toast', ['type' => $submitted ? 'success' : 'info', 'message' => $submitted + ? 'Commit submitted. It goes through the usual review process.' + : 'This commit was already submitted or canceled. Refresh to see its status.']); + + return back(); + } +} diff --git a/app/Http/Requests/CreateScheduledCommitSubmissionRequest.php b/app/Http/Requests/CreateScheduledCommitSubmissionRequest.php new file mode 100644 index 000000000..b23d5934a --- /dev/null +++ b/app/Http/Requests/CreateScheduledCommitSubmissionRequest.php @@ -0,0 +1,25 @@ +route('scheduledCommit'); + assert($scheduled instanceof ScheduledCommit); + + return $this->user()?->id === $scheduled->user_id; + } + + /** @return array{} */ + public function rules(): array + { + return []; + } +} diff --git a/resources/js/pages/scheduled-commits/index.tsx b/resources/js/pages/scheduled-commits/index.tsx index 24bcdb584..361288a72 100644 --- a/resources/js/pages/scheduled-commits/index.tsx +++ b/resources/js/pages/scheduled-commits/index.tsx @@ -3,6 +3,7 @@ import { ArrowLeft, Clock3, ImageIcon, LockKeyhole } from 'lucide-react'; import { useState } from 'react'; import LogController from '@/actions/App/Http/Controllers/LogController'; import ScheduledCommitController from '@/actions/App/Http/Controllers/ScheduledCommitController'; +import ScheduledCommitSubmissionController from '@/actions/App/Http/Controllers/ScheduledCommitSubmissionController'; import { FoldedCommitBody } from '@/components/commit-body'; import { Button } from '@/components/ui/button'; import { useNow } from '@/hooks/use-now'; @@ -34,7 +35,10 @@ export default function Index({ }; }) { const now = useNow(); - const [canceling, setCanceling] = useState(null); + const [busy, setBusy] = useState<{ + id: string; + action: 'commit' | 'cancel'; + } | null>(null); usePoll(30_000, backgroundPollOptions({ only: ['scheduledCommits'] })); const labels = { @@ -90,6 +94,12 @@ export default function Index({ const due = new Date(scheduled.scheduled_at).getTime() <= now.getTime(); + const when = new Date( + scheduled.scheduled_at, + ).toLocaleString(dateLocale, { + dateStyle: 'medium', + timeStyle: 'short', + }); return (
  • - {new Date( - scheduled.scheduled_at, - ).toLocaleString(dateLocale, { - dateStyle: 'medium', - timeStyle: 'short', - })} + {when} {scheduled.status === 'scheduled' && ( - +
    + + +
    )} {scheduled.body && ( @@ -184,7 +223,7 @@ export default function Index({

    {due ? 'Waiting for the scheduler. If submission fails, it is retried automatically.' - : 'Private until submitted. You can cancel before submission.'} + : 'Private until submitted. You can commit it now or cancel it before submission.'}

    )}
  • diff --git a/routes/web.php b/routes/web.php index 2b7e08984..11b4e8e5b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -84,6 +84,7 @@ use App\Http\Controllers\ReportedCommitController; use App\Http\Controllers\SafetyController; use App\Http\Controllers\ScheduledCommitController; +use App\Http\Controllers\ScheduledCommitSubmissionController; use App\Http\Controllers\SearchController; use App\Http\Controllers\SessionController; use App\Http\Controllers\SiteActivityController; @@ -280,6 +281,8 @@ Route::post('scheduled-commits', [ScheduledCommitController::class, 'store']) ->middleware([PreventPostingInTimeout::class, RequireCommitRules::class, ThrottleCommits::class])->name('scheduled-commit.store'); Route::delete('scheduled-commits/{scheduledCommit}', [ScheduledCommitController::class, 'destroy'])->name('scheduled-commit.destroy'); + Route::post('scheduled-commits/{scheduledCommit}/submission', [ScheduledCommitSubmissionController::class, 'store']) + ->middleware(PreventPostingInTimeout::class)->name('scheduled-commit-submission.store'); Route::get('commit-stars', [CommitStarCountController::class, 'index'])->name('commit-star-count.index'); Route::post('commits', [CommitController::class, 'store']) diff --git a/tests/Feature/ScheduledCommitsTest.php b/tests/Feature/ScheduledCommitsTest.php index 67839b43a..003f3e6e1 100644 --- a/tests/Feature/ScheduledCommitsTest.php +++ b/tests/Feature/ScheduledCommitsTest.php @@ -16,6 +16,7 @@ use App\Models\Poll; use App\Models\ScheduledCommit; use App\Models\User; +use App\Models\UserTimeout; use App\Queries\UserActivityQuery; use Illuminate\Database\QueryException; use Illuminate\Http\UploadedFile; @@ -155,6 +156,7 @@ $this->get(route('scheduled-commit.index'))->assertRedirectToRoute('home'); $this->post(route('scheduled-commit.store'), [])->assertRedirectToRoute('home'); $this->delete(route('scheduled-commit.destroy', $scheduled))->assertRedirectToRoute('home'); + $this->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirectToRoute('home'); }); it('rejects invalid times and bodies', function (array $payload, string $field): void { @@ -494,3 +496,47 @@ expect(collect(DB::getQueryLog())->filter(fn (array $query): bool => str_contains($query['query'], 'scheduled_commits')))->toBeEmpty(); }); + +it('commits a scheduled item now, before its time, through the usual review', function (): void { + $scheduled = ScheduledCommit::factory()->create(['scheduled_at' => now()->addDay(), 'body' => 'Shipping early']); + + $this->actingAs($scheduled->user)->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirect() + ->assertInertiaFlash('toast.type', 'success'); + + $commit = Commit::query()->sole(); + expect($commit->body)->toBe('Shipping early') + ->and($commit->status)->toBe(CommitStatus::Pending) + ->and($scheduled->fresh()->commit_id)->toBe($commit->id) + ->and($scheduled->fresh()->submitted_at->equalTo(now()))->toBeTrue(); + + $this->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirect() + ->assertInertiaFlash('toast.type', 'info'); + $this->travel(1)->day(); + $this->artisan('commits:submit-scheduled')->assertSuccessful(); + expect(Commit::query()->count())->toBe(1); +}); + +it('does not commit now a canceled item or another authors item', function (): void { + $canceled = ScheduledCommit::factory()->create(); + resolve(DeleteScheduledCommit::class)->handle($canceled->user, $canceled); + $other = ScheduledCommit::factory()->create(); + + $this->actingAs($canceled->user)->post(route('scheduled-commit-submission.store', $canceled))->assertRedirect() + ->assertInertiaFlash('toast.type', 'info'); + $this->post(route('scheduled-commit-submission.store', $other))->assertForbidden(); + $this->actingAs(User::factory()->create(['username' => 'admin']))->post(route('scheduled-commit-submission.store', $other))->assertForbidden(); + + expect(Commit::query()->count())->toBe(0) + ->and($other->fresh()->submitted_at)->toBeNull(); +}); + +it('does not commit now while the author is in timeout', function (): void { + $scheduled = ScheduledCommit::factory()->create(); + UserTimeout::factory()->for($scheduled->user)->create(); + + $this->actingAs($scheduled->user)->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirect() + ->assertInertiaFlash('timeout'); + + expect(Commit::query()->count())->toBe(0) + ->and(resolve(UpdateScheduledCommitSubmittedAt::class)->handle($scheduled->id, early: true))->toBeFalse(); +}); From 41e159450982b3245d92cc2a3a0383c3ac817405 Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 01:16:29 +0200 Subject: [PATCH 2/5] feat: cancel a scheduled commit with a trash icon Co-Authored-By: Claude Opus 5.5 --- .../js/pages/scheduled-commits/index.tsx | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/resources/js/pages/scheduled-commits/index.tsx b/resources/js/pages/scheduled-commits/index.tsx index 361288a72..40a623c2d 100644 --- a/resources/js/pages/scheduled-commits/index.tsx +++ b/resources/js/pages/scheduled-commits/index.tsx @@ -1,11 +1,22 @@ import { Head, Link, router, usePoll } from '@inertiajs/react'; -import { ArrowLeft, Clock3, ImageIcon, LockKeyhole } from 'lucide-react'; +import { + ArrowLeft, + Clock3, + ImageIcon, + LockKeyhole, + Trash2, +} from 'lucide-react'; import { useState } from 'react'; import LogController from '@/actions/App/Http/Controllers/LogController'; import ScheduledCommitController from '@/actions/App/Http/Controllers/ScheduledCommitController'; import ScheduledCommitSubmissionController from '@/actions/App/Http/Controllers/ScheduledCommitSubmissionController'; import { FoldedCommitBody } from '@/components/commit-body'; import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; import { useNow } from '@/hooks/use-now'; import AppLayout from '@/layouts/app-layout'; import { backgroundPollOptions } from '@/lib/background-poll'; @@ -151,33 +162,40 @@ export default function Index({ ? 'Committing…' : 'Commit now'} - + + + + + + Cancel + + )} From 8a2af426c5b853dc4304135d7059b1bb2fa1140f Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 09:37:26 +0100 Subject: [PATCH 3/5] fix: keep a scheduled commit in place after committing it now Rows keep the order they had when the page opened, and a row sent with Commit now keeps showing what was sent, so the list no longer shifts under the reader. Focus returns to the row, or to Commit now when the request fails. Co-Authored-By: Claude Opus 5.5 --- .../js/pages/scheduled-commits/index.tsx | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/resources/js/pages/scheduled-commits/index.tsx b/resources/js/pages/scheduled-commits/index.tsx index 40a623c2d..551578fcb 100644 --- a/resources/js/pages/scheduled-commits/index.tsx +++ b/resources/js/pages/scheduled-commits/index.tsx @@ -6,7 +6,7 @@ import { LockKeyhole, Trash2, } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import LogController from '@/actions/App/Http/Controllers/LogController'; import ScheduledCommitController from '@/actions/App/Http/Controllers/ScheduledCommitController'; import ScheduledCommitSubmissionController from '@/actions/App/Http/Controllers/ScheduledCommitSubmissionController'; @@ -50,8 +50,50 @@ export default function Index({ id: string; action: 'commit' | 'cancel'; } | null>(null); + const returnFocusTo = useRef(null); + const [sent, setSent] = useState(new Map()); + // Rows keep the order they had when the page opened, and a row sent with Commit now keeps showing what was sent + // (submission clears it), so it stays where it was at its size instead of jumping below the outstanding ones and + // pulling the rows under it up. A fresh visit sorts and trims them again. + const [order] = useState(() => + scheduledCommits.data.map((scheduled) => scheduled.id), + ); + const rank = (id: string) => { + const index = order.indexOf(id); + + return index === -1 ? order.length : index; + }; + const rows = [...scheduledCommits.data] + .sort((a, b) => rank(a.id) - rank(b.id)) + .map((scheduled) => { + const before = sent.get(scheduled.id); + + return before && scheduled.status !== 'scheduled' + ? { + ...before, + submitted_at: scheduled.submitted_at, + status: scheduled.status, + } + : scheduled; + }); usePoll(30_000, backgroundPollOptions({ only: ['scheduledCommits'] })); + useEffect(() => { + if (busy !== null || returnFocusTo.current === null) { + return; + } + + const row = document.querySelector( + `[data-scheduled-commit="${returnFocusTo.current}"]`, + ); + returnFocusTo.current = null; + // The page has the new props by now: back on Commit now when the commit is still outstanding, on its row once + // the button is gone. + (row?.querySelector('[data-commit-now]') ?? row)?.focus({ + preventScroll: true, + }); + }, [busy]); + const labels = { pending: 'Submitted for review', approved: 'Approved', @@ -101,7 +143,7 @@ export default function Index({ ) : (
      - {scheduledCommits.data.map((scheduled) => { + {rows.map((scheduled) => { const due = new Date(scheduled.scheduled_at).getTime() <= now.getTime(); @@ -114,7 +156,9 @@ export default function Index({ return (
    • @@ -139,7 +183,16 @@ export default function Index({ size="sm" disabled={busy !== null} aria-label={`Commit now instead of at ${when}`} + data-commit-now onClick={() => { + returnFocusTo.current = + scheduled.id; + setSent((current) => + new Map(current).set( + scheduled.id, + scheduled, + ), + ); setBusy({ id: scheduled.id, action: 'commit', @@ -237,12 +290,20 @@ export default function Index({ : `${scheduled.image_count} images`}

      )} - {scheduled.status === 'scheduled' && ( + {scheduled.status === 'scheduled' ? (

      {due ? 'Waiting for the scheduler. If submission fails, it is retried automatically.' : 'Private until submitted. You can commit it now or cancel it before submission.'}

      + ) : ( + sent.has(scheduled.id) && ( +

      + Submitted just now, ahead of its + scheduled time. It goes through the + usual review process. +

      + ) )}
    • ); From d8e52b370ccd48a6a0e25443aba57c76799303fe Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 17:14:16 +0100 Subject: [PATCH 4/5] fix: keep a committed-now row only when the server claimed it The controller flashes the id it claimed, and the page drops the row it kept from what was sent unless that id comes back. A row the refreshed page leaves out, because submitted rows sort after older ones, stays from what was sent. Co-Authored-By: Claude Opus 5.5 --- .../ScheduledCommitSubmissionController.php | 4 ++ .../js/pages/scheduled-commits/index.tsx | 71 ++++++++++++------- resources/js/types/global.d.ts | 1 + tests/Feature/ScheduledCommitsTest.php | 6 +- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/app/Http/Controllers/ScheduledCommitSubmissionController.php b/app/Http/Controllers/ScheduledCommitSubmissionController.php index c3fe04832..e4ba429a1 100644 --- a/app/Http/Controllers/ScheduledCommitSubmissionController.php +++ b/app/Http/Controllers/ScheduledCommitSubmissionController.php @@ -16,6 +16,10 @@ public function store(CreateScheduledCommitSubmissionRequest $request, Scheduled { $submitted = $action->handle($scheduledCommit->id, early: true); + if ($submitted) { + Inertia::flash('submitted', $scheduledCommit->id); + } + Inertia::flash('toast', ['type' => $submitted ? 'success' : 'info', 'message' => $submitted ? 'Commit submitted. It goes through the usual review process.' : 'This commit was already submitted or canceled. Refresh to see its status.']); diff --git a/resources/js/pages/scheduled-commits/index.tsx b/resources/js/pages/scheduled-commits/index.tsx index 551578fcb..519cac76d 100644 --- a/resources/js/pages/scheduled-commits/index.tsx +++ b/resources/js/pages/scheduled-commits/index.tsx @@ -54,7 +54,8 @@ export default function Index({ const [sent, setSent] = useState(new Map()); // Rows keep the order they had when the page opened, and a row sent with Commit now keeps showing what was sent // (submission clears it), so it stays where it was at its size instead of jumping below the outstanding ones and - // pulling the rows under it up. A fresh visit sorts and trims them again. + // pulling the rows under it up. Submitted rows sort after older ones, so the refreshed page can leave it out: it + // stays from what was sent. A fresh visit sorts and trims them again. const [order] = useState(() => scheduledCommits.data.map((scheduled) => scheduled.id), ); @@ -63,7 +64,15 @@ export default function Index({ return index === -1 ? order.length : index; }; - const rows = [...scheduledCommits.data] + const missing = [...sent.values()] + .filter( + (before) => + !scheduledCommits.data.some( + (scheduled) => scheduled.id === before.id, + ), + ) + .map((before) => ({ ...before, status: 'pending' as const })); + const rows = [...scheduledCommits.data, ...missing] .sort((a, b) => rank(a.id) - rank(b.id)) .map((scheduled) => { const before = sent.get(scheduled.id); @@ -94,6 +103,36 @@ export default function Index({ }); }, [busy]); + const commitNow = (scheduled: ScheduledCommit) => { + let claimed = false; + returnFocusTo.current = scheduled.id; + // Kept before the response renders, and dropped again unless the server says this request claimed it. + setSent((current) => new Map(current).set(scheduled.id, scheduled)); + setBusy({ id: scheduled.id, action: 'commit' }); + router.post( + ScheduledCommitSubmissionController.store(scheduled.id), + {}, + { + preserveScroll: true, + onSuccess: (page) => { + claimed = page.flash.submitted === scheduled.id; + }, + onFinish: () => { + if (!claimed) { + setSent((current) => { + const next = new Map(current); + next.delete(scheduled.id); + + return next; + }); + } + + setBusy(null); + }, + }, + ); + }; + const labels = { pending: 'Submitted for review', approved: 'Approved', @@ -184,31 +223,9 @@ export default function Index({ disabled={busy !== null} aria-label={`Commit now instead of at ${when}`} data-commit-now - onClick={() => { - returnFocusTo.current = - scheduled.id; - setSent((current) => - new Map(current).set( - scheduled.id, - scheduled, - ), - ); - setBusy({ - id: scheduled.id, - action: 'commit', - }); - router.post( - ScheduledCommitSubmissionController.store( - scheduled.id, - ), - {}, - { - preserveScroll: true, - onFinish: () => - setBusy(null), - }, - ); - }} + onClick={() => + commitNow(scheduled) + } > {busy?.id === scheduled.id && busy.action === 'commit' diff --git a/resources/js/types/global.d.ts b/resources/js/types/global.d.ts index 76c7a78a4..1382a50b0 100644 --- a/resources/js/types/global.d.ts +++ b/resources/js/types/global.d.ts @@ -17,6 +17,7 @@ declare module '@inertiajs/core' { deleted?: DeletedFlash; restored?: Commit; scheduled?: boolean; + submitted?: string; localSignIn?: boolean; signedUp?: boolean; }; diff --git a/tests/Feature/ScheduledCommitsTest.php b/tests/Feature/ScheduledCommitsTest.php index 003f3e6e1..a3fb4cf9f 100644 --- a/tests/Feature/ScheduledCommitsTest.php +++ b/tests/Feature/ScheduledCommitsTest.php @@ -501,7 +501,7 @@ $scheduled = ScheduledCommit::factory()->create(['scheduled_at' => now()->addDay(), 'body' => 'Shipping early']); $this->actingAs($scheduled->user)->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirect() - ->assertInertiaFlash('toast.type', 'success'); + ->assertInertiaFlash('toast.type', 'success')->assertInertiaFlash('submitted', $scheduled->id); $commit = Commit::query()->sole(); expect($commit->body)->toBe('Shipping early') @@ -522,7 +522,7 @@ $other = ScheduledCommit::factory()->create(); $this->actingAs($canceled->user)->post(route('scheduled-commit-submission.store', $canceled))->assertRedirect() - ->assertInertiaFlash('toast.type', 'info'); + ->assertInertiaFlash('toast.type', 'info')->assertInertiaFlashMissing('submitted'); $this->post(route('scheduled-commit-submission.store', $other))->assertForbidden(); $this->actingAs(User::factory()->create(['username' => 'admin']))->post(route('scheduled-commit-submission.store', $other))->assertForbidden(); @@ -535,7 +535,7 @@ UserTimeout::factory()->for($scheduled->user)->create(); $this->actingAs($scheduled->user)->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirect() - ->assertInertiaFlash('timeout'); + ->assertInertiaFlash('timeout')->assertInertiaFlashMissing('submitted'); expect(Commit::query()->count())->toBe(0) ->and(resolve(UpdateScheduledCommitSubmittedAt::class)->handle($scheduled->id, early: true))->toBeFalse(); From 1205a9fa2523ec45166304c035ed044aed652dcf Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 21:44:30 +0100 Subject: [PATCH 5/5] fix: count committing a scheduled commit now against the commit rate limit Co-Authored-By: Claude Opus 5.5 --- routes/web.php | 2 +- tests/Feature/ScheduledCommitsTest.php | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/routes/web.php b/routes/web.php index 11b4e8e5b..8b5018685 100644 --- a/routes/web.php +++ b/routes/web.php @@ -282,7 +282,7 @@ ->middleware([PreventPostingInTimeout::class, RequireCommitRules::class, ThrottleCommits::class])->name('scheduled-commit.store'); Route::delete('scheduled-commits/{scheduledCommit}', [ScheduledCommitController::class, 'destroy'])->name('scheduled-commit.destroy'); Route::post('scheduled-commits/{scheduledCommit}/submission', [ScheduledCommitSubmissionController::class, 'store']) - ->middleware(PreventPostingInTimeout::class)->name('scheduled-commit-submission.store'); + ->middleware([PreventPostingInTimeout::class, ThrottleCommits::class])->name('scheduled-commit-submission.store'); Route::get('commit-stars', [CommitStarCountController::class, 'index'])->name('commit-star-count.index'); Route::post('commits', [CommitController::class, 'store']) diff --git a/tests/Feature/ScheduledCommitsTest.php b/tests/Feature/ScheduledCommitsTest.php index a3fb4cf9f..360007334 100644 --- a/tests/Feature/ScheduledCommitsTest.php +++ b/tests/Feature/ScheduledCommitsTest.php @@ -217,6 +217,7 @@ it('shares the existing commit rate limit', function (): void { $user = User::factory()->create(); + $scheduled = ScheduledCommit::factory()->for($user)->create(); for ($i = 0; $i < 10; $i++) { $this->actingAs($user)->post(route('commit.store'), ['body' => 'An ordinary commit.']); } @@ -224,7 +225,10 @@ $this->post(route('scheduled-commit.store'), [ 'body' => 'Must be throttled.', 'scheduled_at' => now()->addDay()->toIso8601String(), ])->assertRedirect(); - expect(ScheduledCommit::query()->count())->toBe(0); + $this->post(route('scheduled-commit-submission.store', $scheduled))->assertRedirect() + ->assertInertiaFlash('toast.type', 'error')->assertInertiaFlashMissing('submitted'); + expect(ScheduledCommit::query()->count())->toBe(1) + ->and($scheduled->fresh()->submitted_at)->toBeNull(); }); it('keeps schedules out of logs profiles activity previews and moderation until due', function (): void {