Skip to content

Commit 94d71a8

Browse files
Fix PHPStan errors and add integration tests for Alt_Text_Command
Add php-stubs/wp-cli-stubs for PHPStan to recognize WP-CLI classes. Add cli\progress\Bar ignore pattern since the WP-CLI progress bar internals are not in the stubs package. Fix type annotations and add unreachable returns after WP_CLI::error() for static analysis. Add 10 integration tests covering: admin user setup, attachment ID querying (with/without alt text, force flag, specific IDs, invalid IDs), memory cleanup, summary output, and dry-run display.
1 parent abe4259 commit 94d71a8

File tree

5 files changed

+269
-10
lines changed

5 files changed

+269
-10
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"szepeviktor/phpstan-wordpress": "^2.0.2",
2121
"wp-coding-standards/wpcs": "^3.2.0",
2222
"wp-phpunit/wp-phpunit": "^6.9",
23-
"yoast/phpunit-polyfills": "^4.0"
23+
"yoast/phpunit-polyfills": "^4.0",
24+
"php-stubs/wp-cli-stubs": "^2.12"
2425
},
2526
"autoload": {
2627
"psr-4": {

composer.lock

Lines changed: 48 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/CLI/Alt_Text_Command.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,21 @@ class Alt_Text_Command {
7171
*
7272
* @when after_wp_load
7373
*
74-
* @param array $args Positional arguments.
75-
* @param array $assoc_args Associative arguments.
74+
* @param array<int, string> $args Positional arguments.
75+
* @param array<string, mixed> $assoc_args Associative arguments.
7676
*/
7777
public function generate( $args, $assoc_args ): void {
7878
$this->ensure_admin_user();
7979

8080
$ability = wp_get_ability( 'ai/alt-text-generation' );
8181
if ( ! $ability ) {
8282
WP_CLI::error( 'The ai/alt-text-generation ability is not registered. Make sure the Alt Text Generation experiment is enabled in Settings > AI.' );
83+
return; // WP_CLI::error() exits, but this satisfies static analysis.
8384
}
8485

8586
if ( ! has_valid_ai_credentials() ) {
8687
WP_CLI::error( 'No valid AI credentials found. Configure a provider in Settings > AI.' );
88+
return; // WP_CLI::error() exits, but this satisfies static analysis.
8789
}
8890

8991
$batch_size = (int) Utils\get_flag_value( $assoc_args, 'batch-size', 20 );
@@ -183,7 +185,10 @@ static function ( int $id ): bool {
183185

184186
$query = new \WP_Query( $query_args );
185187

186-
return array_map( 'absint', $query->posts );
188+
/** @var int[] $ids */
189+
$ids = $query->posts;
190+
191+
return $ids;
187192
}
188193

189194
/**
@@ -209,7 +214,7 @@ private function display_dry_run( array $attachment_ids ): void {
209214
/**
210215
* Processes images through the alt text generation ability.
211216
*
212-
* @param object $ability The alt text generation ability.
217+
* @param \WP_Ability $ability The alt text generation ability.
213218
* @param int[] $attachment_ids Array of attachment IDs.
214219
* @param int $batch_size Number of images per batch.
215220
* @param int $delay_ms Delay in milliseconds between API calls.
@@ -224,7 +229,7 @@ private function process_images( $ability, array $attachment_ids, int $batch_siz
224229
'failed' => 0,
225230
);
226231
$progress = Utils\make_progress_bar( 'Generating alt text', count( $attachment_ids ) );
227-
$batches = array_chunk( $attachment_ids, $batch_size );
232+
$batches = array_chunk( $attachment_ids, max( 1, $batch_size ) );
228233

229234
foreach ( $batches as $batch ) {
230235
foreach ( $batch as $id ) {

phpstan.neon.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ parameters:
1717
phpVersion:
1818
min: 70400
1919
max: 80400
20+
scanFiles:
21+
- vendor/php-stubs/wp-cli-stubs/wp-cli-stubs.php
2022
bootstrapFiles:
2123
- ai.php
2224
paths:
@@ -28,6 +30,7 @@ parameters:
2830
- '#WP_AI_Client_[A-Za-z0-9_]+#'
2931
- '#WordPress\\AiClient\\#'
3032
- '#Function wp_get_connectors not found\.#'
33+
- '#class cli\\progress\\Bar#'
3134
excludePaths:
3235
analyse:
3336
- tests/
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
/**
3+
* Integration tests for the Alt_Text_Command WP-CLI class.
4+
*
5+
* @package WordPress\AI\Tests\Integration\Includes\CLI
6+
*/
7+
8+
namespace WordPress\AI\Tests\Integration\Includes\CLI;
9+
10+
use WP_UnitTestCase;
11+
use WordPress\AI\CLI\Alt_Text_Command;
12+
13+
/**
14+
* Alt_Text_Command test case.
15+
*
16+
* @covers \WordPress\AI\CLI\Alt_Text_Command
17+
*
18+
* @since x.x.x
19+
*/
20+
class Alt_Text_CommandTest extends WP_UnitTestCase {
21+
22+
/**
23+
* The command instance.
24+
*
25+
* @var \WordPress\AI\CLI\Alt_Text_Command
26+
*/
27+
private $command;
28+
29+
/**
30+
* Set up test case.
31+
*/
32+
public function setUp(): void {
33+
parent::setUp();
34+
35+
$this->command = new Alt_Text_Command();
36+
}
37+
38+
/**
39+
* Tear down test case.
40+
*/
41+
public function tearDown(): void {
42+
wp_set_current_user( 0 );
43+
parent::tearDown();
44+
}
45+
46+
/**
47+
* Invokes a private method on the command instance via reflection.
48+
*
49+
* @param string $method_name The method name to invoke.
50+
* @param array $args Arguments to pass to the method.
51+
* @return mixed The method return value.
52+
*/
53+
private function invoke_private_method( string $method_name, array $args = array() ) {
54+
$reflection = new \ReflectionClass( $this->command );
55+
$method = $reflection->getMethod( $method_name );
56+
57+
if ( PHP_VERSION_ID < 80100 ) {
58+
$method->setAccessible( true );
59+
}
60+
61+
return $method->invoke( $this->command, ...$args );
62+
}
63+
64+
/**
65+
* Test ensure_admin_user sets an admin when no user is set.
66+
*/
67+
public function test_ensure_admin_user_sets_admin(): void {
68+
$this->factory->user->create( array( 'role' => 'administrator' ) );
69+
70+
wp_set_current_user( 0 );
71+
$this->assertEquals( 0, get_current_user_id() );
72+
73+
$this->invoke_private_method( 'ensure_admin_user' );
74+
75+
$this->assertNotEquals( 0, get_current_user_id() );
76+
$this->assertTrue( current_user_can( 'upload_files' ) );
77+
}
78+
79+
/**
80+
* Test ensure_admin_user does not change user when already set.
81+
*/
82+
public function test_ensure_admin_user_preserves_existing_user(): void {
83+
$user_id = $this->factory->user->create( array( 'role' => 'editor' ) );
84+
wp_set_current_user( $user_id );
85+
86+
$this->invoke_private_method( 'ensure_admin_user' );
87+
88+
$this->assertEquals( $user_id, get_current_user_id() );
89+
}
90+
91+
/**
92+
* Test get_attachment_ids returns only images without alt text.
93+
*/
94+
public function test_get_attachment_ids_returns_images_without_alt_text(): void {
95+
$image_without_alt = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
96+
$image_with_alt = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
97+
update_post_meta( $image_with_alt, '_wp_attachment_image_alt', 'Existing alt text' );
98+
99+
$ids = $this->invoke_private_method( 'get_attachment_ids', array( '', false ) );
100+
101+
$this->assertContains( $image_without_alt, $ids );
102+
$this->assertNotContains( $image_with_alt, $ids );
103+
}
104+
105+
/**
106+
* Test get_attachment_ids with force flag includes images with alt text.
107+
*/
108+
public function test_get_attachment_ids_force_includes_all_images(): void {
109+
$image_without_alt = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
110+
$image_with_alt = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
111+
update_post_meta( $image_with_alt, '_wp_attachment_image_alt', 'Existing alt text' );
112+
113+
$ids = $this->invoke_private_method( 'get_attachment_ids', array( '', true ) );
114+
115+
$this->assertContains( $image_without_alt, $ids );
116+
$this->assertContains( $image_with_alt, $ids );
117+
}
118+
119+
/**
120+
* Test get_attachment_ids with specific IDs filters to valid images only.
121+
*/
122+
public function test_get_attachment_ids_with_specific_ids(): void {
123+
$image_id = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
124+
$post_id = $this->factory->post->create();
125+
126+
$ids = $this->invoke_private_method( 'get_attachment_ids', array( "$image_id,$post_id,99999", false ) );
127+
128+
$this->assertContains( $image_id, $ids );
129+
$this->assertNotContains( $post_id, $ids );
130+
$this->assertNotContains( 99999, $ids );
131+
}
132+
133+
/**
134+
* Test get_attachment_ids returns empty for IDs flag with no valid images.
135+
*/
136+
public function test_get_attachment_ids_with_invalid_ids_returns_empty(): void {
137+
$ids = $this->invoke_private_method( 'get_attachment_ids', array( '99999,88888', false ) );
138+
139+
$this->assertEmpty( $ids );
140+
}
141+
142+
/**
143+
* Test get_attachment_ids returns empty when all images have alt text.
144+
*/
145+
public function test_get_attachment_ids_returns_empty_when_all_have_alt(): void {
146+
$image_id = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
147+
update_post_meta( $image_id, '_wp_attachment_image_alt', 'Has alt text' );
148+
149+
$ids = $this->invoke_private_method( 'get_attachment_ids', array( '', false ) );
150+
151+
$this->assertNotContains( $image_id, $ids );
152+
}
153+
154+
/**
155+
* Test stop_the_insanity clears the query log.
156+
*/
157+
public function test_stop_the_insanity_clears_query_log(): void {
158+
global $wpdb;
159+
160+
$wpdb->queries = array( 'some query' );
161+
162+
$this->invoke_private_method( 'stop_the_insanity' );
163+
164+
$this->assertEmpty( $wpdb->queries );
165+
}
166+
167+
/**
168+
* Test print_summary outputs correct format.
169+
*/
170+
public function test_print_summary_runs_without_error(): void {
171+
if ( ! class_exists( 'WP_CLI' ) ) {
172+
$this->markTestSkipped( 'WP-CLI is not available.' );
173+
}
174+
175+
$stats = array(
176+
'generated' => 5,
177+
'decorative' => 1,
178+
'skipped' => 2,
179+
'failed' => 1,
180+
);
181+
182+
ob_start();
183+
$this->invoke_private_method( 'print_summary', array( $stats ) );
184+
ob_end_clean();
185+
186+
// If we got here without error, the method works correctly.
187+
$this->assertTrue( true );
188+
}
189+
190+
/**
191+
* Test display_dry_run outputs table without errors.
192+
*/
193+
public function test_display_dry_run_runs_without_error(): void {
194+
if ( ! class_exists( 'WP_CLI' ) ) {
195+
$this->markTestSkipped( 'WP-CLI is not available.' );
196+
}
197+
198+
$image_id = $this->factory->attachment->create_upload_object( TESTS_REPO_ROOT_DIR . '/tests/data/sample.png' );
199+
200+
ob_start();
201+
$this->invoke_private_method( 'display_dry_run', array( array( $image_id ) ) );
202+
$output = ob_get_clean();
203+
204+
$this->assertStringContainsString( (string) $image_id, $output );
205+
}
206+
}

0 commit comments

Comments
 (0)