Creating a cron job over a connection with no tenant in context
(gateway-token / CLI / master-scope UI session) panicked with a
nil-pointer dereference.
Root cause — tenant asymmetry between insert and readback:
- AddJob INSERT uses tenantIDForInsert(ctx), which falls back to
MasterTenantID when the context has no tenant (row inserted OK).
- The readback (scanJob / GetJob) rejects a nil-tenant context with
"tenant_id required", so GetJob returned (nil, false) and AddJob
swallowed it as (nil, nil).
- handleCreate then dereferenced job.ID (UpdateJob patch) → panic.
Fix (defense-in-depth):
- handleCreate (internal/gateway/methods/cron.go): add a nil-job guard
before any job.ID use; return ErrInternal instead of panicking. Apply
the post-create stateless/command patch under a tenant-consistent
context (WithTenantID(ctx, job.TenantID)) so lockCronJobForMutation
finds the row for nil-tenant callers.
- AddJob (store/pg + store/sqlitestore cron_crud.go): read back under
WithTenantID(ctx, tenantIDForInsert(ctx)) so insert and readback share
the same tenant; return an explicit error if readback fails instead of
swallowing it.
Tests:
- internal/store/sqlitestore/cron_crud_test.go: AddJob with a nil-tenant
context returns a non-nil job + nil error (real SQLite DB).
- internal/gateway/methods/cron_test.go: handleCreate does not panic and
skips UpdateJob when the store returns (nil, nil).
Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
- Add missing reloadClaimed=false arg to executeOneJob call in cron toggle test
- Fix schema migration tests that applied full schema then rolled back version,
causing duplicate column errors on re-migration. Now recreates table without
post-targetVersion columns before re-running migrations.
- recompute next_run_at transactionally on toggle/update instead of non-atomic read/compute/write
- keep repeated EnableJob(true) idempotent by preserving existing next_run_at
- reject re-enabling expired one-shot at jobs with ErrCronJobNoFutureRun
- claim due jobs conditionally, reload before execution, conditional writeback
- align PG and SQLite behavior with shared helpers (DRY)
- atomic scheduler writeback via single UPDATE with CASE expression
Closes#378