Files
prautoblogger/tests/unit/Core/RerunEligibilityTest.php
peptiderepo ac2f20de7e feat: M3 edit + single-step re-run — input versioning, human-modified audit, staged re-run (v0.20.0) (#161)
QA: REJECT@6ed639d (CTO rebase header P1) -> APPROVE@6727e07 (qa-reviews/prautoblogger/2026-06-12-6727e07.md)
CPO guardrails 1-5 verified hard-AC. CEO conditional greenlight 2026-06-12 in chat (QA-APPROVE condition met).

Agent-Session: cto-m3-merge-20260612
2026-06-12 21:46:49 +07:00

183 lines
6.5 KiB
PHP

<?php
/**
* Tests for PRAutoBlogger_Rerun_Eligibility (v0.20.0, M3).
*
* Locks the published-post lockout (CPO guardrail 5 incl. future/private),
* the actively-executing-run block, the editable-stage policy, and the
* downstream/rebuild set computation that drives stale marking.
*
* @package PRAutoBlogger\Tests\Core
*/
namespace PRAutoBlogger\Tests\Core;
use PRAutoBlogger\Tests\BaseTestCase;
use Brain\Monkey\Functions;
class RerunEligibilityTest extends BaseTestCase {
/**
* @var \PHPUnit\Framework\MockObject\MockObject Mock $wpdb.
*/
private $wpdb;
protected function setUp(): void {
parent::setUp();
$this->wpdb = $this->create_mock_wpdb();
$this->wpdb->options = 'wp_options';
$GLOBALS['wpdb'] = $this->wpdb;
$this->wpdb->method( 'prepare' )->willReturnCallback(
static function ( $sql, ...$args ) {
return $sql . ' /* ' . implode( ',', array_map( 'strval', array_filter( $args, 'is_scalar' ) ) ) . ' */';
}
);
\PRAutoBlogger_Run_State::flush_cache();
\PRAutoBlogger_Stage_Input_Store::flush_cache();
}
protected function tearDown(): void {
\PRAutoBlogger_Run_State::flush_cache();
\PRAutoBlogger_Stage_Input_Store::flush_cache();
unset( $GLOBALS['wpdb'] );
parent::tearDown();
}
/**
* Wire the mock for: runs table exists, run row with given status,
* generation lock free.
*/
private function wire_run( string $run_status ): void {
$this->wpdb->method( 'get_var' )->willReturnCallback(
static function ( $sql ) {
$sql = (string) $sql;
if ( false !== strpos( $sql, 'SHOW TABLES' ) ) {
// Both runs + stage_inputs probes succeed.
if ( false !== strpos( $sql, 'stage_inputs' ) ) {
return 'wp_prautoblogger_stage_inputs';
}
return 'wp_prautoblogger_runs';
}
return null; // Lock option absent -> lock free; seed absent.
}
);
$this->wpdb->method( 'get_row' )->willReturn(
array(
'run_id' => 'run-1',
'status' => $run_status,
'ceiling_usd' => '0.50',
)
);
}
/** Stub get_post to return a post with the given status. */
private function stub_post( string $status ): void {
$post = new \WP_Post();
$post->ID = 99;
$post->post_status = $status;
Functions\when( 'get_post' )->justReturn( $post );
}
/**
* Published, scheduled, and private posts are all frozen — no
* edit+rerun (CPO guardrail 5). Draft and pending are not.
*/
public function test_published_post_lockout_covers_all_frozen_statuses(): void {
foreach ( array( 'publish', 'future', 'private' ) as $frozen ) {
$post = new \WP_Post();
$post->post_status = $frozen;
$this->assertTrue( \PRAutoBlogger_Rerun_Eligibility::post_frozen( $post ), "status {$frozen} must freeze" );
}
foreach ( array( 'draft', 'pending' ) as $open ) {
$post = new \WP_Post();
$post->post_status = $open;
$this->assertFalse( \PRAutoBlogger_Rerun_Eligibility::post_frozen( $post ), "status {$open} must not freeze" );
}
// A run with no post yet is not frozen.
$this->assertFalse( \PRAutoBlogger_Rerun_Eligibility::post_frozen( null ) );
}
/**
* check() rejects a published post with the WP-editor redirect copy
* (server-side enforcement, not just hidden UI).
*/
public function test_check_rejects_published_post(): void {
$this->wire_run( 'done' );
$this->stub_post( 'publish' );
$verdict = \PRAutoBlogger_Rerun_Eligibility::check( 'run-1', 99 );
$this->assertFalse( $verdict['ok'] );
$this->assertStringContainsString( 'published', $verdict['reason'] );
}
/**
* check() rejects an actively executing run — "in progress" in the
* CPO sense means unpublished workflow state, not mid-execution.
*/
public function test_check_rejects_actively_executing_run(): void {
$this->wire_run( 'running' );
$this->stub_post( 'draft' );
$verdict = \PRAutoBlogger_Rerun_Eligibility::check( 'run-1', 99 );
$this->assertFalse( $verdict['ok'] );
$this->assertStringContainsString( 'currently executing', $verdict['reason'] );
}
/**
* A terminal run on an unpublished post is eligible: done, failed
* and halted (held) runs can all be re-run.
*/
public function test_check_accepts_terminal_runs_on_unpublished_posts(): void {
foreach ( array( 'done', 'failed', 'halted' ) as $status ) {
$wpdb = $this->create_mock_wpdb();
$wpdb->options = 'wp_options';
$wpdb->method( 'prepare' )->willReturnCallback( static fn( $sql, ...$a ) => (string) $sql );
$wpdb->method( 'get_var' )->willReturnCallback(
static function ( $sql ) {
return ( false !== strpos( (string) $sql, 'SHOW TABLES' ) ) ? 'wp_prautoblogger_runs' : null;
}
);
$wpdb->method( 'get_row' )->willReturn( array( 'status' => $status ) );
$GLOBALS['wpdb'] = $wpdb;
\PRAutoBlogger_Run_State::flush_cache();
$this->stub_post( 'draft' );
$verdict = \PRAutoBlogger_Rerun_Eligibility::check( 'run-1', 99 );
$this->assertTrue( $verdict['ok'], "run status {$status} must be eligible: {$verdict['reason']}" );
}
}
/**
* Editable-stage policy: writer chat stages only. review/analysis/
* image stages get the replay affordance refused at the policy layer.
*/
public function test_editable_stage_policy(): void {
foreach ( array( 'outline', 'draft', 'polish' ) as $stage ) {
$this->assertTrue( \PRAutoBlogger_Rerun_Eligibility::is_editable_stage( $stage ) );
}
foreach ( array( 'review', 'publish', 'analysis', 'research', 'llm_research', 'image_a', 'image_b', 'curate' ) as $stage ) {
$this->assertFalse( \PRAutoBlogger_Rerun_Eligibility::is_editable_stage( $stage ) );
}
$verdict = \PRAutoBlogger_Rerun_Eligibility::check_replay( 'run-1', 99, 'review', 'editor', 'idea:abc' );
$this->assertFalse( $verdict['ok'] );
}
/**
* Downstream/rebuild sets follow the canonical chain — these sets
* drive stale marking and demotion, so order and bounds matter.
*/
public function test_downstream_and_rebuild_sets(): void {
$this->assertSame( array( 'polish', 'review', 'publish' ), \PRAutoBlogger_Rerun_Eligibility::downstream_of( 'draft' ) );
$this->assertSame( array( 'publish' ), \PRAutoBlogger_Rerun_Eligibility::downstream_of( 'review' ) );
$this->assertSame( array(), \PRAutoBlogger_Rerun_Eligibility::downstream_of( 'publish' ) );
$this->assertSame( array(), \PRAutoBlogger_Rerun_Eligibility::downstream_of( 'image_a' ) );
$this->assertSame( array( 'review', 'publish' ), \PRAutoBlogger_Rerun_Eligibility::rebuild_set( 'review' ) );
$this->assertSame(
array( 'outline', 'draft', 'polish', 'review', 'publish' ),
\PRAutoBlogger_Rerun_Eligibility::rebuild_set( 'outline' )
);
}
}