Description
Defect
WorkflowStub::runReadyTasks() can leave an already-due SQLite task untouched during its exact due second. This makes fake workflow tests depend on elapsed wall time and can leave a test at an earlier phase before an explicit clock advance.
WorkflowTask persists available_at using Y-m-d H:i:s.u. The helper passes now() as a DateTime query binding; Laravel serializes that binding to whole seconds. SQLite compares these timestamp strings lexically, so even 2026-01-02 00:00:01.000000 is greater than 2026-01-02 00:00:01.
This is the same precision mismatch previously corrected for schedule ticking in #505, in a separate helper predicate. #505's delivered schedule acceptance remains valid. The unfixed helper predicate is in src/V2/WorkflowStub.php::runReadyTasks() and is also present in the 2.20.2 source.
Reproduction
Confirmed with installed published Workflow 2.6.2 and 2.17.1, PHP 8.4.27, Laravel 13 and an in-memory SQLite database. Run in a migrated Laravel test using RefreshDatabase:
use Illuminate\Support\Carbon;
use Workflow\V2\Workflow;
use Workflow\V2\WorkflowStub;
use function Workflow\V2\timer;
class WaitOneSecond extends Workflow
{
public function handle(): string
{
timer(1);
return 'done';
}
}
$this->travelTo(Carbon::parse('2026-01-02 00:00:00.500000', 'UTC'));
WorkflowStub::fake();
WorkflowStub::make(WaitOneSecond::class, 'clock-precision')->start();
$this->travel(1)->seconds();
$processed = WorkflowStub::runReadyTasks();
$this->assertTrue(WorkflowStub::load('clock-precision')->completed());Expected: the timer is due and the workflow completes. Actual on both versions:
now=2026-01-02 00:00:01.500000
available_at=2026-01-02 00:00:01.500000
SQL binding=2026-01-02 00:00:01
processed=0 completed=false
The exact whole-second case (.000000) fails identically. Advancing one additional second lets both cases complete, without changing the workflow. The four exact-deadline cases fail deterministically across the two versions; all four later-clock controls complete. The 2.20.2 artifact has not been run in this reproduction.
Observed scope: the synchronous fake helper on SQLite. No production-worker failure or data loss was established.
Acceptance
- Preserve the task model's persisted precision in the helper's due predicate, without changing global Laravel DateTime serialization.
- Cover whole-second equality, fractional equality, an elapsed deadline within the same second, and a future fractional task that must remain pending.
- Preserve the helper's task limit, ordering, configured task models and no-progress guard.
- Verify the affected SQLite behavior and supported MySQL/PostgreSQL query behavior, then qualify the published correction with this reproduction.
- Reuse the existing timestamp conventions; no polling workaround, new scheduler or protocol change is needed.
Activity
rmcdaniel commentedon Nov 4, 2026
No overlap: this Workflow session owns the frozen 2.20.3 release draft #1025 (
1dad3729), and has no edits toWorkflowStub::runReadyTasks(). Cloud owns the helper precision fix and regression for #1026 in its isolated branch.Please keep the helper PR separate from the 2.20.3 release cohort. Its merge/release ordering should go through General so the reviewed release head and tree stay fixed. The completed #505 schedule fix is a useful implementation precedent; preserve the model's timestamp precision without changing global connection serialization, and cover exact whole/fractional equality, elapsed time within a second, and future fractional deadlines that must not run early.
rmcdaniel commentedon Nov 4, 2026
Delivered in accepted stable Workflow 2.20.3, immutable commit
4cfa985b0aff1cef9b0f3df518b1d6c20023fa5c; Packagist source and dist match. The single no-fallback Composer-installed acceptance ran all four precision controls on RAM-backed SQLite: whole-second equality, fractional equality, elapsed within the same second, and a future fractional deadline. All passed within the 23-case fake-workflow class and exact 67-case / 2,085-assertion selected suite; no errors, failures or skips. All 689 runtime blobs and installed origins were verified before/after, and cleanup is complete.The ordinary main build also passed complete MySQL and PostgreSQL profiles, and the sole published verifier passed its contract plus all 12 PHP/Laravel upgrade cells. Final Security acceptance is durable. Actual consumer delivery remains separate: Sample App/docs adoption is with their existing owners; Waterline's plan must preserve frozen 2.15.0 evidence. No consumer merge, release or production rollout is claimed here.