Pipeline v2 Phase 1, commit 4/4. Run_Stage_State persists per-run per-stage state keyed run_id + stage + agent_role + item_key (item_key scopes articles because one run_id spans a whole batch; agent_role is the Phase-2 quorum dimension, schema-only). Done stages snapshot output and are reused on re-entry — writer stages via Content_Generator::set_run_item, the review via its stored verdict, a published item skipped outright — never re-run, never re-charged. Post creation is keyed by _prautoblogger_run_id + new _prautoblogger_idea_hash (check-before-insert in Publisher) so retries cannot duplicate posts. Run-level research/analysis checkpoints + done/failed lifecycle marks ride Pipeline_Runner/Executor (incl. orphan-abort + budget-abort paths). Run_Reaper rides the EXISTING #19 daily cron: stages stuck running >2x expected stage wall-clock and runs silent >2x expected run wall-clock -> failed (filterable), open stages reaped, queryable as 'incomplete'. Retention: request_json + stage output payloads NULLed after R days, R from the new Audit Payload Retention setting (default constant 14, 0 = keep forever, no literal at point of use). Audit_Writer records editorial verdicts (run_decisions) and per-article kept sources (run_sources) for new runs. Under-300: status-transient/summary helpers extracted verbatim to Pipeline_Status (Pipeline_Runner 271; removed the worker's duplicate). Agent-Session: pipeline-v2-phase1-engineer-20260612
116 lines
3.5 KiB
PHP
116 lines
3.5 KiB
PHP
<?php
|
|
/**
|
|
* Tests for PRAutoBlogger_Run_Reaper.
|
|
*
|
|
* Locks the retention contract (R days from the SETTING, 0 = keep
|
|
* forever) and the stuck-sweep SQL shapes (2x expected wall-clock,
|
|
* running-only).
|
|
*
|
|
* @package PRAutoBlogger\Tests\Core
|
|
*/
|
|
|
|
namespace PRAutoBlogger\Tests\Core;
|
|
|
|
use PRAutoBlogger\Tests\BaseTestCase;
|
|
use Brain\Monkey\Functions;
|
|
|
|
class RunReaperTest extends BaseTestCase {
|
|
|
|
/**
|
|
* @var \PHPUnit\Framework\MockObject\MockObject Mock $wpdb.
|
|
*/
|
|
private $wpdb;
|
|
|
|
/**
|
|
* Captured SQL passed through $wpdb->query().
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private array $queries = array();
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
$this->queries = array();
|
|
// Local mock: the reaper also uses get_col(), which the shared
|
|
// BaseTestCase mock does not declare.
|
|
$this->wpdb = $this->getMockBuilder( \stdClass::class )
|
|
->addMethods( array( 'prepare', 'get_var', 'get_results', 'get_col', 'insert', 'query', 'get_row', 'update' ) )
|
|
->getMock();
|
|
$this->wpdb->prefix = 'wp_';
|
|
$this->wpdb->insert_id = 0;
|
|
$this->wpdb->last_error = '';
|
|
$GLOBALS['wpdb'] = $this->wpdb;
|
|
$this->wpdb->method( 'prepare' )->willReturnCallback(
|
|
static function ( $sql, ...$args ) {
|
|
return $sql . ' /* ' . implode( ',', array_map( 'strval', $args ) ) . ' */';
|
|
}
|
|
);
|
|
Functions\when( 'apply_filters' )->returnArg( 2 );
|
|
\PRAutoBlogger_Run_State::flush_cache();
|
|
\PRAutoBlogger_Run_Stage_State::flush_cache();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
\PRAutoBlogger_Run_State::flush_cache();
|
|
\PRAutoBlogger_Run_Stage_State::flush_cache();
|
|
unset( $GLOBALS['wpdb'] );
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* Retention 0 (or negative) means "keep forever": the pruner must not
|
|
* issue any UPDATE.
|
|
*/
|
|
public function test_zero_retention_keeps_payloads_forever(): void {
|
|
$this->stub_get_option(
|
|
array(
|
|
'prautoblogger_request_json_retention_days' => 0,
|
|
)
|
|
);
|
|
$this->wpdb->method( 'get_var' )->willReturn( null ); // No state tables.
|
|
$this->wpdb->method( 'get_col' )->willReturn( array() );
|
|
$this->wpdb->expects( $this->never() )->method( 'query' );
|
|
|
|
$stats = \PRAutoBlogger_Run_Reaper::reap();
|
|
$this->assertSame( 0, $stats['payloads_pruned'] );
|
|
}
|
|
|
|
/**
|
|
* With the default setting the pruner NULLs request_json older than R
|
|
* days on the generation log (and stage payloads when that table
|
|
* exists). The sweeps only touch 'running'/open rows.
|
|
*/
|
|
public function test_reap_prunes_and_sweeps_with_setting_backed_window(): void {
|
|
$this->stub_get_option(
|
|
array(
|
|
'prautoblogger_request_json_retention_days' => PRAUTOBLOGGER_DEFAULT_REQUEST_JSON_RETENTION_DAYS,
|
|
)
|
|
);
|
|
// Both state tables exist.
|
|
$this->wpdb->method( 'get_var' )->willReturnCallback(
|
|
static function ( $sql ) {
|
|
if ( false !== strpos( (string) $sql, 'run_stages' ) ) {
|
|
return 'wp_prautoblogger_run_stages';
|
|
}
|
|
return 'wp_prautoblogger_runs';
|
|
}
|
|
);
|
|
$this->wpdb->method( 'get_col' )->willReturn( array() ); // No stuck runs.
|
|
$this->wpdb->method( 'query' )->willReturnCallback(
|
|
function ( $sql ) {
|
|
$this->queries[] = (string) $sql;
|
|
return 2;
|
|
}
|
|
);
|
|
|
|
$stats = \PRAutoBlogger_Run_Reaper::reap();
|
|
|
|
$all_sql = implode( "\n", $this->queries );
|
|
$this->assertStringContainsString( 'request_json = NULL', $all_sql );
|
|
$this->assertStringContainsString( 'meta_json = NULL', $all_sql );
|
|
$this->assertStringContainsString( "status = 'failed'", $all_sql );
|
|
$this->assertStringContainsString( "WHERE status = 'running' AND updated_at <", $all_sql );
|
|
$this->assertSame( 4, $stats['payloads_pruned'] ); // 2 rows x 2 tables.
|
|
}
|
|
}
|