Pilot on washanyanya.ru lost the cf7stg_dispatch cron event somewhere
between 21.04 and 01.05 — wp_next_scheduled('cf7stg_dispatch') returned
NULL, queue had 6 pending rows, sent-24h=0, failed=0, no error trail.
Settings/token were intact. The most likely cause is the PUC self-update
to 1.2.0: WordPress plugin upgrades replace files but do NOT call
register_activation_hook, so Activator::activate() never re-ran and any
cron event Activator::deactivate() had cleared (or that was lost for any
other reason) stayed missing.
Two-part fix:
1. Activator::ensure_scheduled() — extracted from the old private
schedule_events(), now public and idempotent. wp_next_scheduled()
short-circuits when events are already queued, so it's safe to call
on every request. Plugin::boot() invokes it on plugins_loaded — every
admin or front-end hit auto-heals lost cron events.
2. Activator::on_upgrade_complete() bound to upgrader_process_complete.
When WordPress finishes upgrading THIS plugin (matched via
plugin_basename + $hook_extra['plugins']), we re-run install_table()
(idempotent dbDelta), seed_silent_drop_options() (uses add_option,
preserves user values), and ensure_scheduled(). guard_requirements
is intentionally skipped — wp_die mid-upgrade would leave admin in
a broken state, and the host obviously meets requirements since the
old version is currently running.
Tests untouched (this is integration code wrapping WP-only APIs); full
suite still 126/126 green.
Changelog entry for 1.2.1 added to readme.txt.
Stable tag and CF7STG_VERSION will be bumped by release.sh.
Refs: cp-1lg
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dispatcher::register_hooks() runs on plugins_loaded, which is AFTER
register_activation_hook fires. The custom recurrence 'cf7stg_every_minute'
was therefore unknown to wp_schedule_event during activation, and the
'cf7stg_dispatch' event silently failed to schedule — queue items never
got dispatched.
Pilot caught this on washanyanya.ru (23 items queued, 0 sent, 0 failed —
no cron hook firing at all).
Adding the filter directly inside Activator::schedule_events() makes the
custom schedule available in the same request as the wp_schedule_event
call, regardless of bootstrap order.
After updating the plugin, deactivate + reactivate is required to
re-trigger Activator::activate() so events get (re)scheduled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>