Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,14 @@ protected function execute_callback( $input ) {
);
}

$tax_label = $this->get_taxonomy_label( $args['taxonomy'] );

// If we have no content, return an error.
if ( empty( $context['content'] ) ) {
return new WP_Error(
'content_not_provided',
esc_html__( 'Content is required to generate taxonomy suggestions.', 'ai' )
/* translators: %s: Taxonomy label (e.g., "Category" or "Tag"). */
sprintf( esc_html__( 'Content is required to generate %s suggestions.', 'ai' ), esc_html( $tax_label ) )
);
}

Expand All @@ -222,7 +225,8 @@ protected function execute_callback( $input ) {
if ( empty( $result ) ) {
return new WP_Error(
'no_results',
esc_html__( 'No taxonomy suggestions were generated.', 'ai' )
/* translators: %s: Taxonomy label (e.g., "Category" or "Tag"). */
sprintf( esc_html__( 'No %s suggestions were generated.', 'ai' ), esc_html( $tax_label ) )
);
}

Expand All @@ -240,7 +244,9 @@ protected function execute_callback( $input ) {
* @return bool|\WP_Error True if the user has permission, WP_Error otherwise.
*/
protected function permission_callback( $args ) {
$post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : null;
$taxonomy = isset( $args['taxonomy'] ) && is_string( $args['taxonomy'] ) ? $args['taxonomy'] : 'post_tag';
$tax_label = $this->get_taxonomy_label( $taxonomy );
$post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : null;

if ( $post_id ) {
$post = get_post( $post_id );
Expand All @@ -258,7 +264,8 @@ protected function permission_callback( $args ) {
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return new WP_Error(
'insufficient_capabilities',
esc_html__( 'You do not have permission to generate taxonomy suggestions for this post.', 'ai' )
/* translators: %s: Taxonomy label (e.g., "Category" or "Tag"). */
sprintf( esc_html__( 'You do not have permission to generate %s suggestions for this post.', 'ai' ), esc_html( $tax_label ) )
);
}

Expand All @@ -270,7 +277,8 @@ protected function permission_callback( $args ) {
// Ensure the user has permission to edit posts in general.
return new WP_Error(
'insufficient_capabilities',
esc_html__( 'You do not have permission to generate taxonomy suggestions.', 'ai' )
/* translators: %s: Taxonomy label (e.g., "Category" or "Tag"). */
sprintf( esc_html__( 'You do not have permission to generate %s suggestions.', 'ai' ), esc_html( $tax_label ) )
);
}

Expand Down Expand Up @@ -743,4 +751,34 @@ private function get_top_terms( string $taxonomy, int $limit = 100 ): array {

return (array) $terms;
}

/**
* Gets the taxonomy label for use in error and status messages.
*
* Defaults to 'taxonomy' if taxonomy does not exist or has no label.
*
* @since x.x.x
*
* @param string $taxonomy The taxonomy slug.
* @return string Taxonomy singular label (e.g., 'Category', 'Tag', 'taxonomy').
*/
private function get_taxonomy_label( string $taxonomy ): string {
$taxonomy = sanitize_key( $taxonomy );

if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
return __( 'Taxonomy', 'ai' );
}

$tax_object = get_taxonomy( $taxonomy );

if ( ! $tax_object instanceof WP_Taxonomy ) {
return __( 'Taxonomy', 'ai' );
}

$label = ! empty( $tax_object->labels->singular_name )
? $tax_object->labels->singular_name
: ( ! empty( $tax_object->labels->name ) ? $tax_object->labels->name : $taxonomy );

return trim( wp_strip_all_tags( (string) $label ) );
}
}
180 changes: 180 additions & 0 deletions tests/Integration/Includes/Abilities/Content_ClassificationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1446,4 +1446,184 @@ static function ( $prompt ) use ( &$captured_prompt ) {

$this->assertStringNotContainsString( '<available-terms>', $captured_prompt, 'Prompt should not contain available terms for allow_new strategy' );
}

/**
* Test get_taxonomy_label returns the taxonomy singular name.
*
* @since x.x.x
*/
public function test_get_taxonomy_label_returns_correct_labels(): void {
Comment thread
dkotter marked this conversation as resolved.
$reflection = new \ReflectionClass( $this->ability );
$method = $reflection->getMethod( 'get_taxonomy_label' );
$method->setAccessible( true );

$category_label = $method->invoke( $this->ability, 'category' );
$this->assertSame( 'Category', $category_label, 'Label for category should be Category' );

$tag_label = $method->invoke( $this->ability, 'post_tag' );
$this->assertSame( 'Tag', $tag_label, 'Label for post_tag should be Tag' );

$unknown_label = $method->invoke( $this->ability, 'nonexistent_taxonomy' );
$this->assertSame( 'Taxonomy', $unknown_label, 'Label for unknown taxonomy should default to Taxonomy' );

$empty_label = $method->invoke( $this->ability, '' );
$this->assertSame( 'Taxonomy', $empty_label, 'Label for an empty taxonomy should default to Taxonomy' );
}

/**
* Test get_taxonomy_label returns the correct label for a custom multi-word taxonomy.
*
* @since x.x.x
*/
public function test_get_taxonomy_label_with_multi_word_custom_taxonomy(): void {
register_taxonomy(
'book_genre',
'post',
array(
'labels' => array(
'singular_name' => 'Book Genre',
'name' => 'Book Genres',
),
)
);

try {
$reflection = new \ReflectionClass( $this->ability );
$method = $reflection->getMethod( 'get_taxonomy_label' );
$method->setAccessible( true );

$label = $method->invoke( $this->ability, 'book_genre' );
$this->assertSame( 'Book Genre', $label, 'Label for custom multi-word taxonomy should be Book Genre' );
} finally {
unregister_taxonomy( 'book_genre' );
}
}

/**
* Test that execute_callback() error messages contain the taxonomy label for category.
*
* @since x.x.x
*/
public function test_execute_callback_content_not_provided_error_contains_taxonomy_label(): void {
$reflection = new \ReflectionClass( $this->ability );
$method = $reflection->getMethod( 'execute_callback' );
$method->setAccessible( true );

$result = $method->invoke( $this->ability, array( 'taxonomy' => 'category' ) );

$this->assertInstanceOf( WP_Error::class, $result );
$this->assertSame( 'content_not_provided', $result->get_error_code() );
$this->assertSame(
'Content is required to generate Category suggestions.',
$result->get_error_message(),
'Error message should contain the taxonomy label'
);
}

/**
* Test that the no_results error message contains the taxonomy label.
*
* Uses a partial mock so generate_suggestions() returns no suggestions
* without making a request to an AI provider.
*
* @since x.x.x
*/
public function test_no_results_error_message_contains_taxonomy_label(): void {
$mock = $this->getMockBuilder( Content_Classification::class )
->setConstructorArgs(
array(
'ai/content-classification',
array(
'label' => $this->experiment->get_label(),
'description' => $this->experiment->get_description(),
),
)
)
->onlyMethods( array( 'generate_suggestions' ) )
->getMock();

$mock->method( 'generate_suggestions' )->willReturn( array() );

$reflection = new \ReflectionClass( $mock );
$method = $reflection->getMethod( 'execute_callback' );
$method->setAccessible( true );

$result = $method->invoke(
$mock,
array(
'taxonomy' => 'category',
'content' => 'Some content that the model finds nothing to suggest for.',
)
);

$this->assertInstanceOf( WP_Error::class, $result );
$this->assertSame( 'no_results', $result->get_error_code() );
$this->assertSame(
'No Category suggestions were generated.',
$result->get_error_message(),
'no_results error message should contain the taxonomy label'
);
}

/**
* Test that the per-post permission error message contains the taxonomy label.
*
* @since x.x.x
*/
public function test_permission_callback_post_error_message_contains_taxonomy_label(): void {
$reflection = new \ReflectionClass( $this->ability );
$method = $reflection->getMethod( 'permission_callback' );
$method->setAccessible( true );

$post_id = $this->factory->post->create(
array(
'post_content' => 'Test content',
'post_status' => 'publish',
)
);

$user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
wp_set_current_user( $user_id );

$result = $method->invoke(
$this->ability,
array(
'post_id' => $post_id,
'taxonomy' => 'category',
)
);

$this->assertInstanceOf( WP_Error::class, $result );
$this->assertSame( 'insufficient_capabilities', $result->get_error_code() );
$this->assertSame(
'You do not have permission to generate Category suggestions for this post.',
$result->get_error_message(),
'Per-post permission error should contain the taxonomy label'
);
}

/**
* Test that permission_callback() falls back to the post_tag label when the taxonomy arg is unusable.
*
* @since x.x.x
*/
public function test_permission_callback_defaults_to_post_tag_label_when_taxonomy_arg_invalid(): void {
$reflection = new \ReflectionClass( $this->ability );
$method = $reflection->getMethod( 'permission_callback' );
$method->setAccessible( true );

wp_set_current_user( 0 );

foreach ( array( array(), array( 'taxonomy' => 42 ) ) as $args ) {
$result = $method->invoke( $this->ability, $args );

$this->assertInstanceOf( WP_Error::class, $result );
$this->assertSame( 'insufficient_capabilities', $result->get_error_code() );
$this->assertSame(
'You do not have permission to generate Tag suggestions.',
$result->get_error_message(),
'Error message should use Tag as the default taxonomy label'
);
}
}
}
Loading