Files
prautoblogger/tests/unit/Core/RunStateReopenTest.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

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 );
}
}