feat: media-search with missing_alt and unused filters
Ruling Q8 caps the unused scan at 20 candidates per call (not the brief's 500/50), and each result (search and unused alike) is filtered to attachments the current user may edit; overview.md lists media-search as a direct tool (ruling Q2). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Media;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Cursor;
|
||||
use LIT\MCP\Integrations\Media\MediaPresenter;
|
||||
use LIT\MCP\Integrations\Media\MediaSearchAbility;
|
||||
use LIT\MCP\Integrations\Media\MediaUsageFinder;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
|
||||
final class MediaSearchAbilityTest extends TestCase
|
||||
{
|
||||
private function presenter(): MediaPresenter
|
||||
{
|
||||
$presenter = Mockery::mock(MediaPresenter::class);
|
||||
$presenter->allows('summary')->andReturnUsing(static fn (WP_Post $a): array => ['id' => $a->ID]);
|
||||
|
||||
return $presenter;
|
||||
}
|
||||
|
||||
/** @return list<WP_Post> */
|
||||
private function attachments(int ...$ids): array
|
||||
{
|
||||
return array_map(static fn (int $id): WP_Post => new WP_Post(['ID' => $id, 'post_type' => 'attachment']), $ids);
|
||||
}
|
||||
|
||||
public function testMissingAltLimitsToImagesWithoutAltText(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\expect('get_posts')->once()->with(Mockery::on(static fn (array $args): bool =>
|
||||
$args['post_type'] === 'attachment'
|
||||
&& $args['post_mime_type'] === 'image'
|
||||
&& $args['s'] === 'team'
|
||||
&& $args['meta_query'] === [
|
||||
'relation' => 'OR',
|
||||
['key' => '_wp_attachment_image_alt', 'compare' => 'NOT EXISTS'],
|
||||
['key' => '_wp_attachment_image_alt', 'value' => '', 'compare' => '='],
|
||||
]
|
||||
&& $args['posts_per_page'] === 21
|
||||
&& $args['offset'] === 0))->andReturn($this->attachments(1, 2));
|
||||
|
||||
$result = (new MediaSearchAbility($this->presenter(), Mockery::mock(MediaUsageFinder::class)))
|
||||
->execute(['search' => 'team', 'missing_alt' => true]);
|
||||
|
||||
self::assertSame([['id' => 1], ['id' => 2]], $result['items']);
|
||||
self::assertNull($result['next_cursor']);
|
||||
self::assertSame([], $result['warnings']);
|
||||
}
|
||||
|
||||
public function testExcludesAttachmentsTheUserCannotEdit(): void
|
||||
{
|
||||
Functions\when('get_posts')->justReturn($this->attachments(1, 2));
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => $id !== 2);
|
||||
|
||||
$result = (new MediaSearchAbility($this->presenter(), Mockery::mock(MediaUsageFinder::class)))
|
||||
->execute([]);
|
||||
|
||||
self::assertSame([['id' => 1]], $result['items']);
|
||||
}
|
||||
|
||||
public function testUnusedFiltersThroughTheUsageFinder(): void
|
||||
{
|
||||
Functions\when('get_posts')->justReturn($this->attachments(1, 2, 3));
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$finder = Mockery::mock(MediaUsageFinder::class);
|
||||
$finder->allows('isUsed')->andReturnUsing(static fn (int $id): bool => $id === 2);
|
||||
|
||||
$result = (new MediaSearchAbility($this->presenter(), $finder))->execute(['unused' => true]);
|
||||
|
||||
self::assertSame([['id' => 1], ['id' => 3]], $result['items']);
|
||||
self::assertNull($result['next_cursor']);
|
||||
self::assertSame([], $result['warnings']);
|
||||
}
|
||||
|
||||
public function testUnusedStopsAtTheLimitWithACursor(): void
|
||||
{
|
||||
Functions\when('get_posts')->justReturn($this->attachments(1, 2, 3));
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$finder = Mockery::mock(MediaUsageFinder::class);
|
||||
$finder->allows('isUsed')->andReturn(false);
|
||||
|
||||
$result = (new MediaSearchAbility($this->presenter(), $finder))->execute(['unused' => true, 'limit' => 1]);
|
||||
|
||||
self::assertSame([['id' => 1]], $result['items']);
|
||||
self::assertSame(1, Cursor::decode($result['next_cursor']));
|
||||
}
|
||||
|
||||
public function testUnusedSkipsAttachmentsTheUserCannotEdit(): void
|
||||
{
|
||||
Functions\when('get_posts')->justReturn($this->attachments(1, 2, 3));
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => $id !== 2);
|
||||
$finder = Mockery::mock(MediaUsageFinder::class);
|
||||
$finder->allows('isUsed')->andReturn(false);
|
||||
|
||||
$result = (new MediaSearchAbility($this->presenter(), $finder))->execute(['unused' => true]);
|
||||
|
||||
self::assertSame([['id' => 1], ['id' => 3]], $result['items']);
|
||||
self::assertNull($result['next_cursor']);
|
||||
}
|
||||
|
||||
public function testUnusedWarnsAndPagesWhenTheScanCapIsReached(): void
|
||||
{
|
||||
Functions\when('get_posts')->justReturn($this->attachments(...range(1, 21)));
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$finder = Mockery::mock(MediaUsageFinder::class);
|
||||
$finder->allows('isUsed')->andReturn(true);
|
||||
|
||||
$result = (new MediaSearchAbility($this->presenter(), $finder))->execute(['unused' => true]);
|
||||
|
||||
self::assertSame([], $result['items']);
|
||||
self::assertSame(20, Cursor::decode($result['next_cursor']));
|
||||
self::assertNotSame([], $result['warnings']);
|
||||
}
|
||||
|
||||
public function testRequiresUploadRights(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
$ability = new MediaSearchAbility($this->presenter(), Mockery::mock(MediaUsageFinder::class));
|
||||
|
||||
self::assertSame('lit_forbidden', $ability->permission([])->get_error_code());
|
||||
self::assertTrue($ability->isDirect());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user