Files
prautoblogger/tests/unit/Admin/DossierActionsRerunTest.php
peptiderepo 6ed639d1db feat(M3): dossier edit+rerun UI, F2/F3, run-list badges — v0.20.0
- Dossier_Actions (4 AJAX endpoints, manage_options+nonce): save fork /
  queue replay / queue rebuild / stage-status poll. Run identity always
  derived server-side from post meta; the AJAX layer validates + queues
  ONLY — all mutations happen in the cron handlers (chained-cron, CPO
  hard constraint). Fork saves lock message structure (count/roles) so
  every version stays replayable.
- Edit panel per editable stage: per-message textareas prefilled from
  the latest fork or the recorded request_json; explicit fork +
  downstream-stale + queued-execution copy; spend strip with ceiling
  warning (filterable fraction, default 0.8). Disabled stages always
  show WHY (policy reasons; missing-input retention copy).
- Stale / human-edited / attempt chips per stage + header badges (M2
  slot filled); dossier polls stage status while a re-run is active
  (board poll interval setting) and re-renders when the run settles.
- F2: sidebar Models & Prompts card (per-stage model/pv/role) + Run
  Spend card. F3: log-only stages (image_a/b, llm_research, ...) render
  from generation_log + the _prautoblogger_image_role attachment grid.
- M2 correctness: dossier is now item-scoped (idea-hash meta) and
  excludes gen_log rows linked to other posts of a multi-article run.
- Run-list visibility (product AC): board cards get a Human-modified
  chip; Review Queue rows get a stale-stages chip linking to the
  dossier. Both via single batched queries (no N+1).
- v0.20.0 + CHANGELOG + ARCHITECTURE #24 (+ file tree, schema tables,
  db-version note) in this commit per versioning discipline.

Agent-Session: engineer-prab-m3-20260612
2026-06-12 14:28:38 +00:00

104 lines
3.4 KiB
PHP

<?php
/**
* Dossier_Actions: rerun + status endpoint tests (v0.20.0, M3).
*
* Locks the chained-cron contract (queue only — ZERO state mutations in
* the AJAX layer) and the read-only stage-status poll shape.
*
* @package PRAutoBlogger\Tests\Admin
*/
namespace PRAutoBlogger\Tests\Admin;
use Brain\Monkey\Functions;
class DossierActionsRerunTest extends DossierActionsTestCase {
/**
* Chained-cron contract: rerun_stage only schedules — no run/stage
* state is mutated by the AJAX layer, and the response says queued.
*/
public function test_rerun_stage_queues_without_any_state_mutation(): void {
$this->wire_eligible_context();
// A saved fork exists for the replay check.
$wpdb_get_row_fork = array(
'version' => '1',
'request_json' => '{"model":"m","messages":[{"role":"user","content":"edited"}]}',
);
// Rebuild get_row: forks resolve, run row terminal.
$_POST = array(
'post_id' => '99',
'stage' => 'draft',
'agent_role' => 'writer',
);
// Replace the get_row wiring (fork now exists).
$wpdb = $this->create_mock_wpdb();
$wpdb->options = 'wp_options';
$wpdb->method( 'prepare' )->willReturnCallback(
static function ( $sql, ...$args ) {
if ( 1 === count( $args ) && is_array( $args[0] ) ) {
$args = $args[0];
}
return $sql . ' /* ' . implode( ',', array_map( 'strval', $args ) ) . ' */';
}
);
$wpdb->method( 'get_var' )->willReturnCallback(
static function ( $sql ) {
if ( false !== strpos( (string) $sql, 'SHOW TABLES' ) ) {
return ( false !== strpos( (string) $sql, 'stage_inputs' ) ) ? 'wp_prautoblogger_stage_inputs' : 'wp_prautoblogger_runs';
}
return null; // Lock free.
}
);
$wpdb->method( 'get_row' )->willReturnCallback(
static function ( $sql ) use ( $wpdb_get_row_fork ) {
if ( false !== strpos( (string) $sql, 'stage_inputs' ) ) {
return $wpdb_get_row_fork;
}
return array(
'run_id' => 'run-1',
'status' => 'done',
);
}
);
$wpdb->method( 'query' )->willReturnCallback(
function ( $sql ) {
$this->queries[] = (string) $sql;
return 1;
}
);
$GLOBALS['wpdb'] = $wpdb;
\PRAutoBlogger_Run_State::flush_cache();
\PRAutoBlogger_Stage_Input_Store::flush_cache();
$actions = new \PRAutoBlogger_Dossier_Actions();
$this->dispatch( array( $actions, 'on_rerun_stage' ) );
$this->assertTrue( $this->json['ok'] );
$this->assertTrue( $this->json['data']['queued'] );
$this->assertCount( 1, $this->scheduled );
$this->assertSame( \PRAutoBlogger_Rerun_Executor::REPLAY_ACTION, $this->scheduled[0]['hook'] );
$this->assertSame( array( 'run-1', 99, 'draft', 'writer', 'idea:abc123' ), array_slice( $this->scheduled[0]['args'], 0, 5 ) );
foreach ( $this->queries as $sql ) {
$this->assertStringNotContainsString( 'UPDATE', $sql, 'the AJAX layer must not mutate state' );
}
}
/**
* stage_status: read-only poll returns run status + per-stage state
* with stale/human_modified flags.
*/
public function test_stage_status_returns_stage_states(): void {
$this->wire_eligible_context();
$this->wpdb->method( 'get_results' )->willReturn( array() );
$_POST = array( 'post_id' => '99' );
$actions = new \PRAutoBlogger_Dossier_Actions();
$this->dispatch( array( $actions, 'on_stage_status' ) );
$this->assertTrue( $this->json['ok'] );
$this->assertArrayHasKey( 'run_status', $this->json['data'] );
$this->assertArrayHasKey( 'stages', $this->json['data'] );
}
}