fix: empty-content guard + finish_reason=length handling + reasoning budget (v0.18.1)
Reasoning models at high effort could spend the entire max_tokens budget on thinking and return zero visible content with finish_reason=length; the empty article flowed writer -> editor (reject) -> empty draft post, with the cost booked as success (2026-06-11 incident: prod run acf24029, draft post 921; diagnosis: convo pipeline-v2-phase1 seq 12). - Empty-completion guard at the provider seam (new OpenRouter_Completion_Guard): empty/whitespace content is a failure; one retry with reasoning disabled (per-call override) when finish_reason=length or reasoning was active, then the stage fails. - finish_reason=length always warning-logged (model, stage, tokens). - Failed/retried attempts booked to generation_log with response_status=error + real token burn; run_decisions verdict rows (retry_reasoning_disabled / failed_empty_completion) for v0.18.0 runs. - Writer-path guard in Content_Generator::execute_stage and belt-and-braces empty check in Publisher::create_post. - Reasoning budget sanity: reasoning.max_tokens cap + completion headroom via new prautoblogger_reasoning_max_tokens setting (default constant 2048, 0 = legacy uncapped effort mode); cap replaces effort when active; governor reservations use the effective ceiling. - Body assembly moved to Request_Builder::build_body (300-line cap). - Tests: guard verdict matrix, builder budget shaping, writer/publisher guards, full provider retry path. Agent-Session: prab-empty-draft-guard-20260611
This commit is contained in:
@@ -36,6 +36,10 @@ class PublisherTest extends BaseTestCase {
|
||||
|
||||
Functions\when( 'sanitize_text_field' )->returnArg();
|
||||
Functions\when( 'wp_kses_post' )->returnArg();
|
||||
// v0.18.1 empty-content guard strips tags before the emptiness check.
|
||||
Functions\when( 'wp_strip_all_tags' )->alias( function ( $text ) {
|
||||
return trim( strip_tags( (string) $text ) );
|
||||
} );
|
||||
Functions\when( 'is_wp_error' )->justReturn( false );
|
||||
Functions\when( 'apply_filters' )->returnArg( 2 );
|
||||
Functions\when( 'absint' )->alias( function ( $val ) { return abs( (int) $val ); } );
|
||||
@@ -295,4 +299,35 @@ class PublisherTest extends BaseTestCase {
|
||||
$publisher = new \PRAutoBlogger_Publisher();
|
||||
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.18.1 belt-and-braces: save_as_draft() with empty content throws
|
||||
* and never reaches wp_insert_post — no empty draft post can be
|
||||
* created (the 2026-06-11 post-921 incident shape).
|
||||
*/
|
||||
public function test_save_as_draft_refuses_empty_content(): void {
|
||||
Functions\expect( 'wp_insert_post' )->never();
|
||||
|
||||
$publisher = new \PRAutoBlogger_Publisher();
|
||||
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$this->expectExceptionMessage( 'generated content is empty' );
|
||||
|
||||
$publisher->save_as_draft( '', $this->idea, $this->review, 'run_empty_draft' );
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.18.1 belt-and-braces: publish() refuses content that is empty
|
||||
* once tags and whitespace are stripped (an empty HTML shell).
|
||||
*/
|
||||
public function test_publish_refuses_whitespace_only_html_content(): void {
|
||||
Functions\expect( 'wp_insert_post' )->never();
|
||||
|
||||
$publisher = new \PRAutoBlogger_Publisher();
|
||||
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$this->expectExceptionMessage( 'generated content is empty' );
|
||||
|
||||
$publisher->publish( "<p> \n\t </p>", $this->idea, $this->review );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user