- GenerationCheckpointRunnerSyncModeTest.php (new, 246 lines): three tests that
genuinely exercise the sync-mode guards --
test_orchestrate_tick_in_sync_mode_does_not_schedule_background_event:
orchestrate_only() now returns 1 idea so execution reaches the guard at
line ~181 (was vacuous -- empty-ideas stub triggered early return at ~148).
Asserts GENERATE_ACTION NOT scheduled, spawn_cron NOT called.
test_generate_tick_sync_mode_suppresses_reschedule (new):
2-idea queue, set_sync_mode(true), on_generate_tick() -- asserts
GENERATE_ACTION NOT in scheduled events, spawn_cron NOT called.
test_generate_tick_async_mode_schedules_generate_action:
async regression guard moved from original file.
- GenerationCheckpointRunnerTest.php (233 lines): removed the two sync-mode
tests to stay under 300-line rule; added docblock pointer to SyncModeTest.
- CHANGELOG.md: merged duplicate [0.22.1] headings into one entry (NF-2).
Production code unchanged. Version stays 0.22.1.
Agent-Session: prab-reliability-test-fix-20260622
247 lines
9.1 KiB
PHP
247 lines
9.1 KiB
PHP
<?php
|
|
/**
|
|
* Sync-mode guard tests for PRAutoBlogger_Generation_Checkpoint_Runner (v0.22.1).
|
|
*
|
|
* Isolated from GenerationCheckpointRunnerTest to keep both files under 300 lines.
|
|
*
|
|
* Contract under test:
|
|
* set_sync_mode(true) -> on_orchestrate_tick() must NOT schedule GENERATE_ACTION
|
|
* or call spawn_cron/fire_cron_now (VPS loop drives ticks directly).
|
|
* NON-VACUOUS: orchestrate_only() returns 1 idea so execution reaches the guard
|
|
* at line ~181, not the empty-ideas early return at ~148.
|
|
* set_sync_mode(true) -> on_generate_tick() with ideas remaining must NOT schedule
|
|
* GENERATE_ACTION or call spawn_cron/fire_cron_now.
|
|
* set_sync_mode(false) -> on_generate_tick() with ideas remaining DOES schedule
|
|
* GENERATE_ACTION (regression guard -- async path unaffected by sync guard).
|
|
*
|
|
* @package PRAutoBlogger\Tests\Core
|
|
*/
|
|
|
|
namespace PRAutoBlogger\Tests\Core;
|
|
|
|
use PRAutoBlogger\Tests\BaseTestCase;
|
|
use Brain\Monkey\Functions;
|
|
|
|
class GenerationCheckpointRunnerSyncModeTest extends BaseTestCase {
|
|
|
|
/** @var string[] Cron hooks captured from wp_schedule_single_event(). */
|
|
private array $scheduled = array();
|
|
|
|
/** @var array<string, mixed> Option writes captured from update_option(). */
|
|
private array $options = array();
|
|
|
|
/** @var bool Tracks whether spawn_cron was called. */
|
|
private bool $spawn_called = false;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
$this->scheduled = array();
|
|
$this->options = array();
|
|
$this->spawn_called = false;
|
|
|
|
Functions\when( 'wp_schedule_single_event' )->alias(
|
|
function ( $ts, $hook, $args = array() ) {
|
|
$this->scheduled[] = $hook;
|
|
return true;
|
|
}
|
|
);
|
|
Functions\when( 'wp_next_scheduled' )->justReturn( false );
|
|
Functions\when( 'spawn_cron' )->alias( function () {
|
|
$this->spawn_called = true;
|
|
return true;
|
|
} );
|
|
Functions\when( 'wp_remote_post' )->justReturn( array() );
|
|
Functions\when( 'site_url' )->justReturn( 'https://example.com' );
|
|
|
|
Functions\when( 'set_transient' )->justReturn( true );
|
|
Functions\when( 'update_option' )->alias(
|
|
function ( $name, $value, $autoload = null ) {
|
|
$this->options[ $name ] = $value;
|
|
return true;
|
|
}
|
|
);
|
|
Functions\when( 'delete_option' )->alias(
|
|
function ( $name ) {
|
|
unset( $this->options[ $name ] );
|
|
return true;
|
|
}
|
|
);
|
|
Functions\when( 'get_option' )->alias(
|
|
function ( $name, $default = false ) {
|
|
return $this->options[ $name ] ?? $default;
|
|
}
|
|
);
|
|
Functions\when( 'wp_generate_uuid4' )->justReturn( 'sync-test-uuid-001' );
|
|
$this->options['prautoblogger_monthly_budget_usd'] = '0';
|
|
Functions\when( 'wp_cache_get' )->justReturn( false );
|
|
Functions\when( 'wp_cache_set' )->justReturn( true );
|
|
Functions\when( 'wp_cache_delete' )->justReturn( true );
|
|
\PRAutoBlogger_Run_State::flush_cache();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::set_sync_mode( false );
|
|
\PRAutoBlogger_Run_State::flush_cache();
|
|
unset( $GLOBALS['wpdb'] );
|
|
parent::tearDown();
|
|
}
|
|
|
|
/** Stub the generation lock to always succeed. */
|
|
private function wire_wpdb_for_lock(): void {
|
|
$wpdb = $this->create_mock_wpdb();
|
|
$wpdb->options = 'wp_options';
|
|
$GLOBALS['wpdb'] = $wpdb;
|
|
$wpdb->method( 'query' )->willReturn( 1 );
|
|
$wpdb->method( 'get_var' )->willReturn( null );
|
|
}
|
|
|
|
/** Build a minimal Article_Idea-compatible array. */
|
|
private function make_idea( string $topic = 'Sync Topic' ): array {
|
|
return array(
|
|
'topic' => $topic,
|
|
'article_type' => 'guide',
|
|
'suggested_title' => 'Guide to ' . $topic,
|
|
'summary' => '',
|
|
'score' => 0.9,
|
|
'analysis_id' => 1,
|
|
'source_ids' => array(),
|
|
'key_points' => array(),
|
|
'target_keywords' => array(),
|
|
);
|
|
}
|
|
|
|
// __ sync-mode guard: on_orchestrate_tick() ______________________________________________
|
|
|
|
/**
|
|
* sync_mode=true + >=1 idea returned: on_orchestrate_tick() must not schedule
|
|
* GENERATE_ACTION or call spawn_cron (VPS loop drives generate ticks directly).
|
|
*
|
|
* NON-VACUOUS: orchestrate_only() returns 1 idea so execution reaches the sync
|
|
* guard at line ~181, not the empty-ideas early return at ~148.
|
|
*/
|
|
public function test_orchestrate_tick_in_sync_mode_does_not_schedule_background_event(): void {
|
|
$this->wire_wpdb_for_lock();
|
|
|
|
// Provide Pipeline_Runner stub that returns 1 idea -- forces execution past
|
|
// the empty-ideas early return so the sync guard at line ~181 is exercised.
|
|
if ( ! class_exists( 'PRAutoBlogger_Pipeline_Runner', false ) ) {
|
|
// phpcs:ignore Squiz.Strings.DoubleQuoteUsage.NotRequired
|
|
eval( 'class PRAutoBlogger_Pipeline_Runner {
|
|
public function set_skip_dedup($v){ return $this; }
|
|
public function orchestrate_only($ct){
|
|
$idea = new PRAutoBlogger_Article_Idea([
|
|
"topic"=>"Sync Topic","article_type"=>"guide",
|
|
"suggested_title"=>"Guide","summary"=>"","score"=>0.9,
|
|
"analysis_id"=>1,"source_ids"=>[],"key_points"=>[],
|
|
"target_keywords"=>[]
|
|
]);
|
|
return [$idea];
|
|
}
|
|
}' );
|
|
}
|
|
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::set_sync_mode( true );
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::on_orchestrate_tick();
|
|
|
|
$this->assertNotContains(
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::GENERATE_ACTION,
|
|
$this->scheduled,
|
|
'sync mode: on_orchestrate_tick must not schedule GENERATE_ACTION'
|
|
);
|
|
$this->assertFalse(
|
|
$this->spawn_called,
|
|
'sync mode: on_orchestrate_tick must not call spawn_cron'
|
|
);
|
|
}
|
|
|
|
// __ sync-mode guard: on_generate_tick() _________________________________________________
|
|
|
|
/**
|
|
* sync_mode=true + 2 ideas in queue: on_generate_tick() must not reschedule
|
|
* GENERATE_ACTION or call spawn_cron (VPS loop is the sole driver of next tick).
|
|
*/
|
|
public function test_generate_tick_sync_mode_suppresses_reschedule(): void {
|
|
$run_id = 'sync-gen-tick-uuid';
|
|
$this->options['prautoblogger_article_queue'] = array(
|
|
'run_id' => $run_id,
|
|
'ideas' => array( $this->make_idea( 'A' ), $this->make_idea( 'B' ) ),
|
|
'results' => array( 'generated' => 0, 'published' => 0, 'rejected' => 0, 'cost' => 0.0 ),
|
|
);
|
|
|
|
$wpdb = $this->create_mock_wpdb();
|
|
$wpdb->options = 'wp_options';
|
|
$GLOBALS['wpdb'] = $wpdb;
|
|
$wpdb->method( 'get_row' )->willReturn( array( 'status' => 'running', 'ceiling_usd' => '0' ) );
|
|
$wpdb->method( 'query' )->willReturn( 1 );
|
|
$wpdb->method( 'get_var' )->willReturn( null );
|
|
|
|
if ( ! class_exists( 'PRAutoBlogger_Article_Worker', false ) ) {
|
|
// phpcs:ignore Squiz.Strings.DoubleQuoteUsage.NotRequired
|
|
eval( 'class PRAutoBlogger_Article_Worker {
|
|
public function __construct($ct){}
|
|
public function generate($idea){ return ["generated"=>1,"published"=>1,"rejected"=>0,"cost"=>0.01]; }
|
|
}' );
|
|
}
|
|
if ( ! class_exists( 'PRAutoBlogger_Post_Assembler', false ) ) {
|
|
// phpcs:ignore Squiz.Strings.DoubleQuoteUsage.NotRequired
|
|
eval( 'class PRAutoBlogger_Post_Assembler { public static function amortize_research_costs($r){} }' );
|
|
}
|
|
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::set_sync_mode( true );
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::on_generate_tick();
|
|
|
|
$this->assertNotContains(
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::GENERATE_ACTION,
|
|
$this->scheduled,
|
|
'sync mode: on_generate_tick must not schedule GENERATE_ACTION'
|
|
);
|
|
$this->assertFalse(
|
|
$this->spawn_called,
|
|
'sync mode: on_generate_tick must not call spawn_cron'
|
|
);
|
|
}
|
|
|
|
// __ async regression guard ______________________________________________________________
|
|
|
|
/**
|
|
* sync_mode=false: on_generate_tick() with ideas remaining DOES schedule
|
|
* GENERATE_ACTION (regression guard -- async path must be unaffected by sync guard).
|
|
*/
|
|
public function test_generate_tick_async_mode_schedules_generate_action(): void {
|
|
$run_id = 'async-regression-uuid';
|
|
$this->options['prautoblogger_article_queue'] = array(
|
|
'run_id' => $run_id,
|
|
'ideas' => array( $this->make_idea( 'A' ), $this->make_idea( 'B' ) ),
|
|
'results' => array( 'generated' => 0, 'published' => 0, 'rejected' => 0, 'cost' => 0.0 ),
|
|
);
|
|
|
|
$wpdb = $this->create_mock_wpdb();
|
|
$wpdb->options = 'wp_options';
|
|
$GLOBALS['wpdb'] = $wpdb;
|
|
$wpdb->method( 'get_row' )->willReturn( array( 'status' => 'running', 'ceiling_usd' => '0' ) );
|
|
$wpdb->method( 'query' )->willReturn( 1 );
|
|
$wpdb->method( 'get_var' )->willReturn( null );
|
|
|
|
if ( ! class_exists( 'PRAutoBlogger_Article_Worker', false ) ) {
|
|
// phpcs:ignore Squiz.Strings.DoubleQuoteUsage.NotRequired
|
|
eval( 'class PRAutoBlogger_Article_Worker {
|
|
public function __construct($ct){}
|
|
public function generate($idea){ return ["generated"=>1,"published"=>1,"rejected"=>0,"cost"=>0.01]; }
|
|
}' );
|
|
}
|
|
if ( ! class_exists( 'PRAutoBlogger_Post_Assembler', false ) ) {
|
|
// phpcs:ignore Squiz.Strings.DoubleQuoteUsage.NotRequired
|
|
eval( 'class PRAutoBlogger_Post_Assembler { public static function amortize_research_costs($r){} }' );
|
|
}
|
|
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::set_sync_mode( false );
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::on_generate_tick();
|
|
|
|
$this->assertContains(
|
|
\PRAutoBlogger_Generation_Checkpoint_Runner::GENERATE_ACTION,
|
|
$this->scheduled,
|
|
'async mode: on_generate_tick must schedule GENERATE_ACTION when ideas remain'
|
|
);
|
|
}
|
|
}
|