fix: wire Image B source data — was hardcoded null, never generated
Root cause: both attach_generated_images() in post-assembler and the retroactive handler in executor passed null as source_data to the image pipeline. The pipeline's Image B branch requires non-null source_data to fire, so Image B was silently skipped on every article since the feature was built. Fix: added get_source_data_for_image() to Source_Collector to retrieve the original Reddit post title/content by source_ids. Post-assembler now pulls source_ids from the Article_Idea; executor pulls them from the _prautoblogger_source_ids post meta saved by the publisher. Also added a regression test proving Image B is skipped when source_data is null, so this handoff gap will be caught if re-introduced. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,13 +135,26 @@ class PRAutoBlogger_Executor {
|
|||||||
'post_content' => $post->post_content,
|
'post_content' => $post->post_content,
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = $pipeline->generate_and_attach_images( $post_id, $article_data, null );
|
// Retrieve original source IDs from post meta so Image B can generate
|
||||||
|
// a source-driven prompt even on retroactive regeneration.
|
||||||
|
$source_ids_json = get_post_meta( $post_id, '_prautoblogger_source_ids', true );
|
||||||
|
$source_ids = is_string( $source_ids_json ) ? json_decode( $source_ids_json, true ) : [];
|
||||||
|
$source_data = ( new PRAutoBlogger_Source_Collector() )->get_source_data_for_image(
|
||||||
|
is_array( $source_ids ) ? array_map( 'absint', $source_ids ) : []
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $pipeline->generate_and_attach_images( $post_id, $article_data, $source_data );
|
||||||
|
|
||||||
// Set featured image if Image A was generated.
|
// Set featured image if Image A was generated.
|
||||||
if ( ! empty( $result['image_a_id'] ) ) {
|
if ( ! empty( $result['image_a_id'] ) ) {
|
||||||
set_post_thumbnail( $post_id, $result['image_a_id'] );
|
set_post_thumbnail( $post_id, $result['image_a_id'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store Image B in post meta if generated.
|
||||||
|
if ( ! empty( $result['image_b_id'] ) ) {
|
||||||
|
update_post_meta( $post_id, '_prautoblogger_image_b_id', $result['image_b_id'] );
|
||||||
|
}
|
||||||
|
|
||||||
wp_send_json_success( $result );
|
wp_send_json_success( $result );
|
||||||
} catch ( \Exception $e ) {
|
} catch ( \Exception $e ) {
|
||||||
PRAutoBlogger_Logger::instance()->error(
|
PRAutoBlogger_Logger::instance()->error(
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ class PRAutoBlogger_Post_Assembler {
|
|||||||
* @param array $post_data Post data array (for article content).
|
* @param array $post_data Post data array (for article content).
|
||||||
*/
|
*/
|
||||||
public static function attach_generated_images( int $post_id, PRAutoBlogger_Article_Idea $idea, array $post_data ): void {
|
public static function attach_generated_images( int $post_id, PRAutoBlogger_Article_Idea $idea, array $post_data ): void {
|
||||||
$source_data = null;
|
// Fetch the original Reddit source data for Image B's source-driven prompt.
|
||||||
|
// The idea's source_ids point to rows in the source_data table collected
|
||||||
|
// during the pipeline's collection step.
|
||||||
|
$source_data = ( new PRAutoBlogger_Source_Collector() )->get_source_data_for_image( $idea->get_source_ids() );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = ( new PRAutoBlogger_Image_Pipeline() )->generate_and_attach_images( $post_id, $post_data, $source_data );
|
$result = ( new PRAutoBlogger_Image_Pipeline() )->generate_and_attach_images( $post_id, $post_data, $source_data );
|
||||||
|
|||||||
@@ -57,6 +57,47 @@ class PRAutoBlogger_Source_Collector {
|
|||||||
return $total_inserted;
|
return $total_inserted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve source data rows by their database IDs.
|
||||||
|
*
|
||||||
|
* Used by the image pipeline to build Image B's source-driven prompt from
|
||||||
|
* the source_ids stored on the Article_Idea. Returns the first matching
|
||||||
|
* row's title and content; these map directly to what
|
||||||
|
* Image_Prompt_Builder::build_source_prompt() expects.
|
||||||
|
*
|
||||||
|
* Side effects: one SELECT query.
|
||||||
|
*
|
||||||
|
* @param int[] $ids Database row IDs from the source_data table.
|
||||||
|
* @return array{title: string, selftext: string}|null First source row, or null if none found.
|
||||||
|
*/
|
||||||
|
public function get_source_data_for_image( array $ids ): ?array {
|
||||||
|
if ( empty( $ids ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'prautoblogger_source_data';
|
||||||
|
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||||
|
$row = $wpdb->get_row(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT title, content, subreddit FROM {$table} WHERE id IN ({$placeholders}) ORDER BY score DESC LIMIT 1",
|
||||||
|
...$ids
|
||||||
|
),
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( null === $row ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $row['title'] ?? 'Reddit Discussion',
|
||||||
|
'selftext' => $row['content'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store collected source data in the database.
|
* Store collected source data in the database.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -130,6 +130,45 @@ class ImagePipelineTest extends BaseTestCase {
|
|||||||
$this->assertGreaterThan( 0.05, $result['cost_usd'] );
|
$this->assertGreaterThan( 0.05, $result['cost_usd'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that Image B is NOT generated when source_data is null.
|
||||||
|
*
|
||||||
|
* This is the regression test for the Image B data-handoff gap:
|
||||||
|
* post_assembler.php and executor.php both passed null as source_data,
|
||||||
|
* so the pipeline silently skipped Image B. If this test ever fails
|
||||||
|
* with `$this->exactly(2)`, it means someone re-broke the handoff.
|
||||||
|
*/
|
||||||
|
public function test_image_b_skipped_when_source_data_is_null(): void {
|
||||||
|
$provider = $this->createMock( \PRAutoBlogger_Image_Provider_Interface::class );
|
||||||
|
$provider->method( 'estimate_cost' )->willReturn( 0.05 );
|
||||||
|
$provider->method( 'generate_image' )->willReturn( [
|
||||||
|
'bytes' => 'fake_image_data',
|
||||||
|
'mime_type' => 'image/png',
|
||||||
|
'width' => 1200,
|
||||||
|
'height' => 630,
|
||||||
|
'model' => 'flux-1-schnell',
|
||||||
|
'cost_usd' => 0.05,
|
||||||
|
'latency_ms' => 2000,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$cost_tracker = $this->createMock( \PRAutoBlogger_Cost_Tracker::class );
|
||||||
|
$cost_tracker->method( 'would_exceed_budget' )->willReturn( false );
|
||||||
|
|
||||||
|
// Only ONE call to log_image_generation — Image A only.
|
||||||
|
$cost_tracker->expects( $this->exactly( 1 ) )->method( 'log_image_generation' );
|
||||||
|
|
||||||
|
$pipeline = new \PRAutoBlogger_Image_Pipeline( $provider, $cost_tracker );
|
||||||
|
|
||||||
|
Functions\when( 'get_temp_dir' )->justReturn( '/tmp/' );
|
||||||
|
Functions\when( 'media_handle_sideload' )->justReturn( 42 );
|
||||||
|
|
||||||
|
// Pass null source_data — Image B must NOT fire.
|
||||||
|
$result = $pipeline->generate_and_attach_images( 1, [ 'post_title' => 'Test' ], null );
|
||||||
|
|
||||||
|
$this->assertArrayHasKey( 'image_a_id', $result );
|
||||||
|
$this->assertArrayNotHasKey( 'image_b_id', $result );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test graceful handling when image generation fails.
|
* Test graceful handling when image generation fails.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user