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
This commit is contained in:
peptiderepo
2026-06-12 13:40:27 +00:00
parent 38500b2781
commit 6ed639d1db
30 changed files with 2606 additions and 22 deletions

View File

@@ -0,0 +1,87 @@
<?php
/**
* Dossier_Actions: save_input endpoint tests (v0.20.0, M3).
*
* Locks: published-post rejection (guardrail 5 server-side), fork
* structure validation (count/roles locked, contents non-empty), and
* the merged-fork save (model/options from the base, never the client).
*
* @package PRAutoBlogger\Tests\Admin
*/
namespace PRAutoBlogger\Tests\Admin;
use Brain\Monkey\Functions;
class DossierActionsSaveInputTest extends DossierActionsTestCase {
/**
* Frozen post: save_input rejects server-side with 400 and inserts
* nothing (guardrail 5 is not just hidden UI).
*/
public function test_save_input_rejects_published_post(): void {
$this->wire_eligible_context( 'publish' );
$_POST = array(
'post_id' => '99',
'stage' => 'draft',
'agent_role' => 'writer',
'messages' => '[]',
);
$actions = new \PRAutoBlogger_Dossier_Actions();
$this->dispatch( array( $actions, 'on_save_input' ) );
$this->assertFalse( $this->json['ok'] );
$this->assertSame( 400, $this->json['status'] );
$this->assertCount( 0, $this->inserted );
}
/**
* Structure lock: changing a message ROLE (or count, or emptying a
* content) is rejected — forks are always replayable chat bodies.
*/
public function test_save_input_rejects_structure_mismatch(): void {
$this->wire_eligible_context();
$_POST = array(
'post_id' => '99',
'stage' => 'draft',
'agent_role' => 'writer',
// Role flipped user->assistant vs the base body.
'messages' => '[{"role":"system","content":"sys"},{"role":"assistant","content":"edited"}]',
);
$actions = new \PRAutoBlogger_Dossier_Actions();
$this->dispatch( array( $actions, 'on_save_input' ) );
$this->assertFalse( $this->json['ok'] );
$this->assertCount( 0, $this->inserted );
}
/**
* Valid edit: fork v1 saved with the edited content merged into the
* base body; model/options come from the base, not the client.
*/
public function test_save_input_saves_merged_fork(): void {
$this->wire_eligible_context();
$_POST = array(
'post_id' => '99',
'stage' => 'draft',
'agent_role' => 'writer',
'messages' => '[{"role":"system","content":"sys"},{"role":"user","content":"EDITED PROMPT"}]',
);
$actions = new \PRAutoBlogger_Dossier_Actions();
$this->dispatch( array( $actions, 'on_save_input' ) );
$this->assertTrue( $this->json['ok'] );
$this->assertSame( 1, $this->json['data']['version'] );
$this->assertCount( 1, $this->inserted );
$row = $this->inserted[0];
$this->assertSame( 'human', $row['source'] );
$this->assertSame( 'rhys', $row['author'] );
$this->assertSame( 'idea:abc123', $row['item_key'] );
$body = json_decode( (string) $row['request_json'], true );
$this->assertSame( 'EDITED PROMPT', $body['messages'][1]['content'] );
$this->assertSame( 'm', $body['model'] ); // From the base, not the client.
}
}