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
103 lines
3.1 KiB
PHP
103 lines
3.1 KiB
PHP
<?php
|
|
/**
|
|
* Tests for PRAutoBlogger_Run_State::reopen() (v0.20.0, M3).
|
|
*
|
|
* Locks: terminal-only atomic gate, ceiling re-snapshot from the
|
|
* CURRENT setting, and — critical for CPO guardrail 4 — that the
|
|
* accumulated reserved/settled spend is never reset, so re-runs are
|
|
* new spend on the same run ledger.
|
|
*
|
|
* @package PRAutoBlogger\Tests\Core
|
|
*/
|
|
|
|
namespace PRAutoBlogger\Tests\Core;
|
|
|
|
use PRAutoBlogger\Tests\BaseTestCase;
|
|
|
|
class RunStateReopenTest extends BaseTestCase {
|
|
|
|
/**
|
|
* @var \PHPUnit\Framework\MockObject\MockObject Mock $wpdb.
|
|
*/
|
|
private $wpdb;
|
|
|
|
/**
|
|
* SQL captured from $wpdb->query().
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private array $queries = array();
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
$this->queries = array();
|
|
$this->wpdb = $this->create_mock_wpdb();
|
|
$GLOBALS['wpdb'] = $this->wpdb;
|
|
$this->wpdb->method( 'get_var' )->willReturn( 'wp_prautoblogger_runs' );
|
|
$this->wpdb->method( 'prepare' )->willReturnCallback(
|
|
static function ( $sql, ...$args ) {
|
|
return $sql . ' /* ' . implode( ',', array_map( 'strval', $args ) ) . ' */';
|
|
}
|
|
);
|
|
$this->stub_get_option( array( 'prautoblogger_per_run_cost_ceiling_usd' => '0.75' ) );
|
|
\PRAutoBlogger_Run_State::flush_cache();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
\PRAutoBlogger_Run_State::flush_cache();
|
|
unset( $GLOBALS['wpdb'] );
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* reopen() transitions ONLY terminal runs (atomic conditional UPDATE)
|
|
* and re-snapshots the ceiling from the current setting.
|
|
*/
|
|
public function test_reopen_gates_on_terminal_status_and_resnapshots_ceiling(): void {
|
|
$this->wpdb->method( 'query' )->willReturnCallback(
|
|
function ( $sql ) {
|
|
$this->queries[] = (string) $sql;
|
|
return 1;
|
|
}
|
|
);
|
|
|
|
$this->assertTrue( \PRAutoBlogger_Run_State::reopen( 'run-1' ) );
|
|
|
|
$sql = $this->queries[0];
|
|
$this->assertStringContainsString( "SET status = 'running'", $sql );
|
|
$this->assertStringContainsString( "status IN ('done','failed','halted')", $sql );
|
|
$this->assertStringContainsString( 'ceiling_usd = %f', $sql );
|
|
$this->assertStringContainsString( '0.75', $sql ); // Current setting value.
|
|
$this->assertStringContainsString( 'finished_at = NULL', $sql );
|
|
// Guardrail 4: accumulated spend is NEVER reset by a reopen.
|
|
$this->assertStringNotContainsString( 'reserved_usd', $sql );
|
|
$this->assertStringNotContainsString( 'settled_usd', $sql );
|
|
$this->assertStringNotContainsString( 'overage_usd', $sql );
|
|
}
|
|
|
|
/**
|
|
* No row matched (run is pending/running or absent) -> false, so the
|
|
* executor aborts instead of replaying against a closed gate.
|
|
*/
|
|
public function test_reopen_returns_false_when_not_terminal(): void {
|
|
$this->wpdb->method( 'query' )->willReturn( 0 );
|
|
|
|
$this->assertFalse( \PRAutoBlogger_Run_State::reopen( 'run-1' ) );
|
|
}
|
|
|
|
/**
|
|
* Self-healing: missing table / empty run id -> false, no query.
|
|
*/
|
|
public function test_reopen_noop_paths(): void {
|
|
$this->wpdb->method( 'query' )->willReturnCallback(
|
|
function ( $sql ) {
|
|
$this->queries[] = (string) $sql;
|
|
return 1;
|
|
}
|
|
);
|
|
|
|
$this->assertFalse( \PRAutoBlogger_Run_State::reopen( '' ) );
|
|
$this->assertCount( 0, $this->queries );
|
|
}
|
|
}
|