diff --git a/CHANGELOG.md b/CHANGELOG.md index 423fc89..fb1c9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ Versioning: [Semantic Versioning](https://semver.org/) --- +## [0.3.1] — 2026-06-15 + +Self-healing cron scheduling: `PRV_Cron_Guard` ensures managed WP-Cron events +are re-scheduled on `init` when missing (e.g. after an rsync deploy that +bypasses the activation hook). + +### Fixed +- Managed cron hooks (`prv_weekly_probe`, `prv_daily_prune`) are now + re-scheduled automatically on any page load where they are missing. + Delegates to `PRV_Cron::schedule()` and `PRV_Prune_Cron::schedule()` so + recurrence and first-run offset are identical to what the activator sets. + Hot-path cost: two `wp_next_scheduled()` checks; no DB writes unless an + event is missing. + +### Added +- **`PRV_Cron_Guard`** (`includes/core/class-prv-cron-guard.php`) — idempotent + guard registered on `init`; calls canonical scheduler methods when missing. +- `PRV_Plugin::init()` — wires `PRV_Cron_Guard::register_hooks()`. +- Tests: `test-cron-guard.php`. + +--- + ## [0.3.0] — 2026-06-15 Per-call cost accountability + LLM call audit trail. Every probe call now diff --git a/includes/core/class-prv-cron-guard.php b/includes/core/class-prv-cron-guard.php new file mode 100644 index 0000000..cbbd045 --- /dev/null +++ b/includes/core/class-prv-cron-guard.php @@ -0,0 +1,69 @@ +register_hooks(); + $guard = new PRV_Cron_Guard(); + $guard->register_hooks(); + if ( is_admin() ) { $page = new PRV_Admin_Page(); $page->register_hooks(); diff --git a/pr-vision.php b/pr-vision.php index 3dde6db..37c953a 100644 --- a/pr-vision.php +++ b/pr-vision.php @@ -3,7 +3,7 @@ * Plugin Name: PR Vision * Plugin URI: https://peptiderepo.com * Description: Weekly server-side LLM probes for AI-visibility (GEO) tracking. Records whether peptiderepo.com is cited by LLMs across core peptides, stores time-series, and renders an admin dashboard + settings UI. - * Version: 0.3.0 + * Version: 0.3.1 * Author: peptiderepo * Author URI: https://peptiderepo.com * License: GPL-2.0-or-later @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { /* ── Constants ────────────────────────────────────────────────────────── */ -define( 'PRV_VERSION', '0.3.0' ); +define( 'PRV_VERSION', '0.3.1' ); define( 'PRV_PLUGIN_FILE', __FILE__ ); define( 'PRV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PRV_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f3e5192..901c02d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -27,7 +27,7 @@ if ( ! defined( 'ARRAY_N' ) ) { define( 'ARRAY_N', 'ARRAY_N' ); } -define( 'PRV_VERSION', '0.3.0' ); +define( 'PRV_VERSION', '0.3.1' ); define( 'PRV_PLUGIN_FILE', __DIR__ . '/../pr-vision.php' ); define( 'PRV_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' ); define( 'PRV_PLUGIN_URL', 'http://example.test/wp-content/plugins/pr-vision/' ); diff --git a/tests/unit/test-cron-guard.php b/tests/unit/test-cron-guard.php new file mode 100644 index 0000000..74a8dcf --- /dev/null +++ b/tests/unit/test-cron-guard.php @@ -0,0 +1,96 @@ +ensure_scheduled(); + +prv_assert( false !== wp_next_scheduled( PRV_CRON_HOOK ), 'guard/weekly: prv_weekly_probe scheduled after ensure_scheduled()' ); + +// ── Test: ensure_scheduled schedules prv_daily_prune when missing ──────── + +prv_assert( false !== wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ), 'guard/prune: prv_daily_prune scheduled after ensure_scheduled()' ); + +// ── Test: idempotent -- called twice, still only one event each ────────── + +$ts_weekly_1 = wp_next_scheduled( PRV_CRON_HOOK ); +$ts_prune_1 = wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ); + +$guard->ensure_scheduled(); // Second call. + +$ts_weekly_2 = wp_next_scheduled( PRV_CRON_HOOK ); +$ts_prune_2 = wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ); + +prv_assert( $ts_weekly_1 === $ts_weekly_2, 'guard/idempotent: weekly event timestamp unchanged after second call' ); +prv_assert( $ts_prune_1 === $ts_prune_2, 'guard/idempotent: prune event timestamp unchanged after second call' ); + +// Verify the test state has exactly one entry per hook (no duplicates). +$cron_events = $GLOBALS['prv_test_state']['cron_events']; +prv_assert( array_key_exists( PRV_CRON_HOOK, $cron_events ), 'guard/idempotent: exactly one weekly event entry' ); +prv_assert( array_key_exists( PRV_Prune_Cron::PRUNE_HOOK, $cron_events ), 'guard/idempotent: exactly one prune event entry' ); +prv_assert( 2 === count( $cron_events ), 'guard/idempotent: only two cron event entries total' ); + +// ── Test: guard uses same recurrence as canonical schedulers ───────────── + +prv_test_reset(); +$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly'; +PRV_Cron::schedule(); +$canonical_weekly = $GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ]['schedule'] ?? null; + +prv_test_reset(); +PRV_Prune_Cron::schedule(); +$canonical_prune = $GLOBALS['prv_test_state']['cron_events'][ PRV_Prune_Cron::PRUNE_HOOK ]['schedule'] ?? null; + +// Now verify guard-scheduled events share the same recurrences. +prv_test_reset(); +$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly'; +$guard2 = new PRV_Cron_Guard(); +$guard2->ensure_scheduled(); + +$guard_weekly = $GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ]['schedule'] ?? null; +$guard_prune = $GLOBALS['prv_test_state']['cron_events'][ PRV_Prune_Cron::PRUNE_HOOK ]['schedule'] ?? null; + +prv_assert_equals( $canonical_weekly, $guard_weekly, 'guard/recurrence: weekly recurrence matches canonical PRV_Cron::schedule()' ); +prv_assert_equals( $canonical_prune, $guard_prune, 'guard/recurrence: prune recurrence matches canonical PRV_Prune_Cron::schedule()' ); + +// ── Test: no-op when both events already scheduled ─────────────────────── + +prv_test_reset(); +$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly'; +PRV_Cron::schedule(); +PRV_Prune_Cron::schedule(); + +$before = $GLOBALS['prv_test_state']['cron_events']; + +$guard3 = new PRV_Cron_Guard(); +$guard3->ensure_scheduled(); + +$after = $GLOBALS['prv_test_state']['cron_events']; + +prv_assert( $before === $after, 'guard/noop: cron_events unchanged when all events already scheduled' ); + +// ── Test: register_hooks adds the init action ──────────────────────────── + +prv_test_reset(); +$guard4 = new PRV_Cron_Guard(); +$guard4->register_hooks(); + +$hooks = array_column( $GLOBALS['prv_test_state']['actions'], 'hook' ); +prv_assert( in_array( 'init', $hooks, true ), 'guard/register_hooks: init action registered' ); + +exit( prv_test_summary() );