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
2 changes: 1 addition & 1 deletion src/tools/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const TEMPLATE_TOOLS = [
{
name: 'list_templates',
description:
'List a block theme\'s templates (page layouts like "single", "archive") or template parts (reusable regions like "header", "footer"). Each row includes `wp_id` — non-null only when a database override shadows the theme file, which is what makes a template editable via update_template. On a classic (non-block) theme, returns an empty list with a `note` explaining why.',
'List the active theme\'s templates (page layouts like "single", "archive") or template parts (reusable regions like "header", "footer") — works on a theme without a full block-theme structure too, as long as it has real templates/parts. Each row includes `wp_id` — non-null only when a database override shadows the theme file, which is what makes a template editable via update_template. Returns an empty list with a `note` only when there is truly nothing to list.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clarify the wp_id contract.

The PHP formatter exposes wp_id whenever the resolved template has that property; it does not require has_theme_file. Describing it as non-null only when an override shadows a theme file overstates the guarantee. Say that it identifies a database-backed template or override.

Based on Template_Manager::format_template_summary(), wp_id is independent of the has_theme_file field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/templates.ts` at line 28, Update the template-listing description
in the relevant tool definition to state that wp_id identifies a database-backed
template or override whenever present. Remove the claim that it is non-null only
when a database override shadows a theme file, since
Template_Manager::format_template_summary() treats wp_id independently of
has_theme_file.

annotations: { ...READ_ANNOT, title: 'List templates' },
inputSchema: {
type: 'object' as const,
Expand Down
2 changes: 1 addition & 1 deletion wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -53578,7 +53578,7 @@ var TEMPLATE_SOURCE_ENUM = ["theme", "plugin", "custom"];
var TEMPLATE_TOOLS = [
{
name: "list_templates",
description: 'List a block theme\'s templates (page layouts like "single", "archive") or template parts (reusable regions like "header", "footer"). Each row includes `wp_id` \u2014 non-null only when a database override shadows the theme file, which is what makes a template editable via update_template. On a classic (non-block) theme, returns an empty list with a `note` explaining why.',
description: 'List the active theme\'s templates (page layouts like "single", "archive") or template parts (reusable regions like "header", "footer") \u2014 works on a theme without a full block-theme structure too, as long as it has real templates/parts. Each row includes `wp_id` \u2014 non-null only when a database override shadows the theme file, which is what makes a template editable via update_template. Returns an empty list with a `note` only when there is truly nothing to list.',
annotations: { ...READ_ANNOT2, title: "List templates" },
inputSchema: {
type: "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2104,7 +2104,7 @@
"name": "list_templates",
"ability": "gk-block-mcp/list-templates",
"label": "List templates",
"description": "List a block theme's templates (page layouts like \"single\", \"archive\") or template parts (reusable regions like \"header\", \"footer\"). Each row includes `wp_id` — non-null only when a database override shadows the theme file, which is what makes a template editable via update_template. On a classic (non-block) theme, returns an empty list with a `note` explaining why.",
"description": "List the active theme's templates (page layouts like \"single\", \"archive\") or template parts (reusable regions like \"header\", \"footer\") — works on a theme without a full block-theme structure too, as long as it has real templates/parts. Each row includes `wp_id` — non-null only when a database override shadows the theme file, which is what makes a template editable via update_template. Returns an empty list with a `note` only when there is truly nothing to list.",
"input_schema": {
"type": "object",
"properties": {
Expand Down
54 changes: 29 additions & 25 deletions wordpress-plugin/gk-block-mcp/includes/class-template-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,6 @@ public function get_templates( array $args ) {
return $type;
}

if ( ! wp_is_block_theme() ) {
return array(
'templates' => array(),
'count' => 0,
'note' => __( 'Active theme is not a block theme; no block templates exist.', 'gk-block-mcp' ),
);
}

$query = array();

if ( 'wp_template_part' === $type && ! empty( $args['area'] ) ) {
Expand Down Expand Up @@ -163,10 +155,20 @@ static function ( $template ) use ( $source ) {

$formatted = array_map( array( $this, 'format_template_summary' ), $templates );

return array(
$result = array(
'templates' => $formatted,
'count' => count( $formatted ),
);

// A hybrid theme (wp_is_block_theme() false, e.g. no templates/index.html)
// can still have real templates/parts get_block_templates() finds via
// theme files or DB overrides, so an empty result alone doesn't mean
// "not a block theme" — only note that when it's also actually true.
if ( empty( $formatted ) && ! wp_is_block_theme() ) {
$result['note'] = __( 'Active theme is not a full block theme; only registered block templates/parts are listed.', 'gk-block-mcp' );
Comment on lines +167 to +168

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Assign conditional checks to named variables.

These conditions inline wp_is_block_theme() and, at Line 167, a compound check. Assign the predicates first, then branch, as required by the repository PHP guidelines.

Proposed pattern
-		if ( empty( $formatted ) && ! wp_is_block_theme() ) {
+		$has_templates  = ! empty( $formatted );
+		$is_block_theme = wp_is_block_theme();
+		if ( ! $has_templates && ! $is_block_theme ) {

Apply the same $is_block_theme pattern in update_template() and reset_template().

As per coding guidelines, checks must be assigned to named variables before conditionals.

Also applies to: 283-284, 394-395

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/includes/class-template-manager.php` around
lines 167 - 168, Assign the wp_is_block_theme() result to a named
$is_block_theme variable before conditionals, and extract the compound
empty($formatted) check into a descriptive predicate before the branch in the
relevant template-listing flow. Apply the same named-variable pattern in
update_template() and reset_template(), covering the corresponding checks near
the referenced sections.

Source: Coding guidelines

}

return $result;
}

/**
Expand Down Expand Up @@ -254,14 +256,6 @@ public function update_template( $id, $type, array $args ) {
return $type;
}

if ( ! wp_is_block_theme() ) {
return new \WP_Error(
'classic_theme',
__( 'Active theme is not a block theme; there are no block templates to edit.', 'gk-block-mcp' ),
array( 'status' => 400 )
);
}

$id = is_string( $id ) ? sanitize_text_field( $id ) : '';
if ( '' === $id ) {
return new \WP_Error(
Expand All @@ -281,8 +275,18 @@ public function update_template( $id, $type, array $args ) {
);
}

// Gate on whether the id actually resolves, not wp_is_block_theme():
// a hybrid theme (no templates/index.html) can still have real,
// resolvable templates/parts, and those are meaningful to edit.
$template = get_block_template( $id, $type );
if ( ! $template ) {
if ( ! wp_is_block_theme() ) {
return new \WP_Error(
'classic_theme',
__( 'Active theme is not a block theme; there are no block templates to edit.', 'gk-block-mcp' ),
array( 'status' => 400 )
);
}
Comment on lines 281 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
git ls-files wordpress-plugin/gk-block-mcp/includes/class-template-manager.php

# Show outline if available, then relevant line ranges
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline wordpress-plugin/gk-block-mcp/includes/class-template-manager.php --view expanded || true
fi

nl -ba wordpress-plugin/gk-block-mcp/includes/class-template-manager.php | sed -n '240,430p'

Repository: GravityKit/block-mcp

Length of output: 973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all occurrences of the error codes and template lookup helpers.
rg -n "classic_theme|not_found|get_block_template|wp_is_block_theme|template-part|footer" wordpress-plugin/gk-block-mcp/includes/class-template-manager.php wordpress-plugin/gk-block-mcp/includes -g '*.php'

Repository: GravityKit/block-mcp

Length of output: 8951


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the public methods that return the template-not-found errors.
python3 - <<'PY'
from pathlib import Path
p = Path('wordpress-plugin/gk-block-mcp/includes/class-template-manager.php')
text = p.read_text()
for needle in ['function', 'classic_theme', 'not_found']:
    pass
# Print the surrounding blocks for methods containing get_block_template
lines = text.splitlines()
for i, line in enumerate(lines, start=1):
    if 'get_block_template' in line or 'classic_theme' in line or 'not_found' in line:
        start = max(1, i-25)
        end = min(len(lines), i+35)
        print(f"\n--- lines {start}-{end} around line {i} ---")
        for n in range(start, end+1):
            print(f"{n:4d}: {lines[n-1]}")
PY

Repository: GravityKit/block-mcp

Length of output: 33623


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the relevant section around the two reported ranges.
nl -ba wordpress-plugin/gk-block-mcp/includes/class-template-manager.php | sed -n '260,420p'

Repository: GravityKit/block-mcp

Length of output: 198


🌐 Web query:

WordPress wp_is_block_theme hybrid theme block templates get_block_template missing ID classic_theme not_found

💡 Result:

In WordPress, the behavior you are observing regarding wp_is_block_theme, get_block_template, and template discovery is rooted in how WordPress distinguishes between "full" block themes and hybrid themes [1][2]. wp_is_block_theme Functionality The function wp_is_block_theme specifically checks whether a theme is a block-based theme [3]. Internally, it wraps wp_get_theme->is_block_theme, which determines this status by checking for the presence of an index.html file in either the /templates/ or /block-templates/ directory of the theme [4][5]. If your hybrid theme relies on traditional PHP templates (e.g., index.php) and does not have an index.html block template at the root, wp_is_block_theme will return false, correctly identifying it as a classic or hybrid theme rather than a pure block theme [4][2]. get_block_template and Template Discovery The function get_block_template retrieves a block template object (WP_Block_Template) by its ID (e.g., 'theme_slug//template_slug') [6]. When you call this function for a template that does not exist in the database (customized via the Site Editor), WordPress falls back to get_block_file_template to look for the template file on the file system [7]. If you are encountering a "not found" or null result when calling get_block_template in a hybrid theme: 1. File Location: Ensure your block template files are correctly placed in the /templates/ or /block-templates/ directories of your theme [4][2]. 2. Theme Support: For hybrid themes to utilize block templates, ensure that the theme has explicitly declared support for them. While block template parts (via the /parts directory) can be used with broad theme support, full block templates may require the theme to be recognized as having block-template capabilities [1][8]. 3. Hybrid vs. Full Block: In a hybrid theme, the PHP template hierarchy (index.php, single.php) still takes precedence [2]. If WordPress locates a PHP template for a given request, it may bypass the block template discovery logic unless you are specifically using functions or hooks that force the rendering of a block template [9]. If you are developing a hybrid theme and need to access a block template file, verify that the ID passed to get_block_template matches the expected 'theme_slug//template_slug' format and that the file resides in the expected directory [6][7]. If you are seeing unexpected nulls, it is likely because the internal discovery mechanism is either failing to find the file due to pathing or because the current page request is resolving to a PHP template file, leading WordPress to skip the block template search [9].

Citations:


🌐 Web query:

WordPress wp_is_block_theme hybrid theme classic theme block templates semantics

💡 Result:

In WordPress, themes are generally categorized based on their architecture and compatibility with Full Site Editing (FSE). While WordPress recognizes two official types—Classic and Block—the term Hybrid is widely used by the developer community to describe themes that bridge these two approaches [1][2]. wp_is_block_theme The function wp_is_block_theme is a boolean helper introduced in WordPress 5.9.0 [3][4]. It returns true if the active theme is a Block theme and false otherwise [3]. Under the hood, it acts as a wrapper for wp_get_theme->is_block_theme [5][4]. Specifically, it determines if a theme is a block-based theme by checking for the presence of an index.html block template (usually located in /templates/ or /block-templates/) [3][5]. Theme Semantics 1. Block Themes: These are designed for Full Site Editing (FSE) [6]. They use HTML-based block templates (stored in a /templates/ directory) and a theme.json file to manage global styles, settings, and layout [7]. In these themes, the Site Editor is the primary tool for customization, replacing traditional Customizer menus and widgets [7][8]. 2. Classic Themes: These rely on the traditional WordPress structure, utilizing PHP template files (e.g., index.php, single.php) and the WordPress Customizer for site-wide settings [6][7]. While they support the block editor for post/page content, they do not use block-based site templates [7]. 3. Hybrid Themes: This is a non-official, community-recognized category [1][9]. A hybrid theme is essentially a classic theme that has adopted selected block features [2]. These themes maintain a classic PHP-based structure but incorporate modern elements such as theme.json, block patterns, or block-based template parts [1][2]. This allows developers to offer modern design flexibility while retaining the stability and specific workflows (like the Customizer) associated with classic themes [10][6]. Summary Table Theme Type | Core Architecture | Primary Customization | FSE Support --- | --- | --- | --- Block | HTML Block Templates | Site Editor [7] | Full Classic | PHP Templates [6] | Customizer [7] | None Hybrid | PHP Templates [6] | Customizer + Blocks [6] | Partial [6] Because wp_is_block_theme checks specifically for the presence of block-based template files [3][5], it will return false for both classic and hybrid themes, as neither relies on the full block-template architecture required for a theme to be classified as a Block theme [1][9].

Citations:


Return not_found for unresolved hybrid templates
! wp_is_block_theme() still routes a missing template/template-part ID to classic_theme even when the active theme already resolves other block templates/parts. Reserve that error for genuine classic themes and return not_found for unresolved IDs here; add regression coverage for missing hybrid template and template-part IDs. Also applies to: 392-400.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/includes/class-template-manager.php` around
lines 281 - 289, Update the unresolved-template handling in the template lookup
flow around get_block_template and the corresponding template-part path: return
the existing not_found WP_Error for missing IDs when the active theme is hybrid,
while reserving classic_theme for genuine classic themes. Add regression
coverage for unresolved hybrid template and template-part IDs, including both
affected branches.

return new \WP_Error(
'not_found',
sprintf( /* translators: %s: template id */ __( 'Template "%s" not found.', 'gk-block-mcp' ), $id ),
Expand Down Expand Up @@ -375,14 +379,6 @@ public function reset_template( $id, $type = 'wp_template' ) {
return $type;
}

if ( ! wp_is_block_theme() ) {
return new \WP_Error(
'classic_theme',
__( 'Active theme is not a block theme; there are no template overrides to reset.', 'gk-block-mcp' ),
array( 'status' => 400 )
);
}

$id = is_string( $id ) ? sanitize_text_field( $id ) : '';
if ( '' === $id ) {
return new \WP_Error(
Expand All @@ -392,8 +388,16 @@ public function reset_template( $id, $type = 'wp_template' ) {
);
}

// Same resolution-based gate as update_template() — see its comment.
$template = get_block_template( $id, $type );
if ( ! $template ) {
if ( ! wp_is_block_theme() ) {
return new \WP_Error(
'classic_theme',
__( 'Active theme is not a block theme; there are no template overrides to reset.', 'gk-block-mcp' ),
array( 'status' => 400 )
);
}
return new \WP_Error(
'not_found',
sprintf( /* translators: %s: template id */ __( 'Template "%s" not found.', 'gk-block-mcp' ), $id ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ private function find_block_theme() {
return null;
}

/**
* Register the fixture theme directory containing "hybrid-theme" (a
* theme with templates/ and parts/ files but no templates/index.html,
* so wp_is_block_theme() is false). A separate root from
* ensure_theme_root_resolvable()'s dummy one; by the time it's
* registered the "more than one root" workaround already applies, so
* this one just needs to contain the fixture.
*
* @return void
*/
private function register_hybrid_theme_root() {
register_theme_directory( dirname( __DIR__ ) . '/fixtures/themes' );
// search_theme_directories() memoizes its scan in a function-local
// static for the rest of the process; by this point in the run
// something has always already forced that memoization without
// this root, so appending it here is invisible until forced.
wp_clean_themes_cache();
}
Comment on lines +98 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep changed PHP comments limited to current behavior and contracts.

  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php#L98-L115: describe fixture-root registration and cache invalidation without run-history rationale.
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php#L319-L325: state the hybrid listing contract without referring to the old short-circuit.
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php#L104-L115: state the local cache-invalidation contract instead of referring to another test file.
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php#L492-L497: state the hybrid write contract without implementation history.
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php#L542-L547: state the classic-theme error contract without “unchanged regression” framing.

As per coding guidelines, “Comments and docblocks must document current behavior and hard contracts” and must omit historical journals and off-tree references.

📍 Affects 2 files
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php#L98-L115 (this comment)
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php#L319-L325
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php#L104-L115
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php#L492-L497
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php#L542-L547
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php` around
lines 98 - 115, Revise comments at
wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php:98-115 and
:319-325, and
wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php:104-115,
:492-497, and :542-547 to describe only current behavior and contracts:
fixture-root registration/cache invalidation, hybrid listing behavior, local
cache invalidation, hybrid writes, and classic-theme errors respectively. Remove
run-history, old implementation, regression, and off-tree references; update
comments/docblocks around the relevant test methods without changing test
behavior.

Source: Coding guidelines


/**
* Find a formatted template row by slug.
*
Expand Down Expand Up @@ -294,4 +313,39 @@ public function test_get_template_invalid_type_returns_error() {
$this->assertInstanceOf( \WP_Error::class, $result );
$this->assertSame( 'invalid_type', $result->get_error_code() );
}

// ── get_templates(): hybrid theme ─────────────────────────────────

/**
* A theme can have real templates and parts on disk without satisfying
* wp_is_block_theme() (which checks specifically for templates/index.html
* or block-templates/index.html). The old unconditional
* `! wp_is_block_theme()` short-circuit hid those templates/parts
* entirely and printed a note claiming none exist.
*/
public function test_get_templates_hybrid_theme_lists_real_templates_and_parts() {
$this->register_hybrid_theme_root();
switch_theme( 'hybrid-theme' );
$this->assertFalse( wp_is_block_theme(), 'Fixture must reproduce wp_is_block_theme() === false to exercise the hybrid case.' );

$templates = $this->tm->get_templates( array( 'type' => 'wp_template' ) );
$this->assertNotNull( $this->find_by_slug( $templates['templates'], 'single' ) );
$this->assertArrayNotHasKey( 'note', $templates );

$parts = $this->tm->get_templates( array( 'type' => 'wp_template_part' ) );
$this->assertNotNull( $this->find_by_slug( $parts['templates'], 'footer' ) );
$this->assertArrayNotHasKey( 'note', $parts );
}

/**
* The note is informational, not a blanket "any empty result" flag: an
* empty result on a real block theme (e.g. an area filter matching
* nothing) must not carry a "not a block theme" note that isn't true.
*/
public function test_get_templates_full_block_theme_empty_result_has_no_note() {
$result = $this->tm->get_templates( array( 'type' => 'wp_template_part', 'area' => 'no-such-area' ) );

$this->assertSame( array(), $result['templates'] );
$this->assertArrayNotHasKey( 'note', $result );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ private function find_block_theme() {
return null;
}

/**
* Register the fixture theme directory containing "hybrid-theme". See
* TemplateManagerTest::register_hybrid_theme_root() for the rationale.
*
* @return void
*/
private function register_hybrid_theme_root() {
register_theme_directory( dirname( __DIR__ ) . '/fixtures/themes' );
// See TemplateManagerTest::register_hybrid_theme_root() — forces
// search_theme_directories()'s memoized scan to pick this root up.
wp_clean_themes_cache();
}

// ── Gate ───────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -473,4 +486,74 @@ public function test_update_template_rolls_back_new_override_when_area_term_assi
);
$this->assertCount( 0, $matching->posts, 'A term-assignment failure must not leave an orphaned override post behind.' );
}

// ── Hybrid theme (wp_is_block_theme() false, but a part resolves) ───

/**
* A hybrid theme's template part resolves via get_block_template(),
* so a gated write against it must succeed — the old unconditional
* `! wp_is_block_theme()` 400 guard blocked this even though the part
* genuinely renders on such a site.
*/
public function test_update_template_creates_override_for_hybrid_theme_template_part() {
update_option( Template_Manager::ALLOW_TEMPLATE_EDITS_OPTION, '1' );
$this->register_hybrid_theme_root();
switch_theme( 'hybrid-theme' );
$this->assertFalse( wp_is_block_theme(), 'Fixture must reproduce wp_is_block_theme() === false to exercise the hybrid case.' );

$result = $this->tm->update_template(
'hybrid-theme//footer',
'wp_template_part',
array( 'content' => '<!-- wp:paragraph --><p>Overridden footer</p><!-- /wp:paragraph -->' )
);

$this->assertIsArray( $result );
$this->assertTrue( $result['success'] );

$fetched = $this->tm->get_template( 'hybrid-theme//footer', 'wp_template_part' );
$this->assertSame( 'custom', $fetched['source'] );
$this->assertStringContainsString( 'Overridden footer', $fetched['content'] );
}

/**
* reset_template must be gated the same way — resolution, not
* wp_is_block_theme() — so it can revert the override this test just
* created on a hybrid theme.
*/
public function test_reset_template_reverts_hybrid_theme_override() {
update_option( Template_Manager::ALLOW_TEMPLATE_EDITS_OPTION, '1' );
$this->register_hybrid_theme_root();
switch_theme( 'hybrid-theme' );

$updated = $this->tm->update_template(
'hybrid-theme//footer',
'wp_template_part',
array( 'content' => '<!-- wp:paragraph --><p>Overridden</p><!-- /wp:paragraph -->' )
);
$this->assertTrue( $updated['success'] );

$reset = $this->tm->reset_template( 'hybrid-theme//footer', 'wp_template_part' );

$this->assertIsArray( $reset );
$this->assertTrue( $reset['success'] );
$this->assertNull( get_post( $updated['wp_id'] ) );
}

/**
* Unchanged regression: a genuinely classic theme (no templates/parts
* at all) still gets the specific, actionable "classic_theme" 400 —
* proves the resolution-based gate doesn't regress into a generic
* not_found for the case that guard exists to make clearer.
*/
public function test_update_template_classic_theme_still_returns_400_when_nothing_resolves() {
update_option( Template_Manager::ALLOW_TEMPLATE_EDITS_OPTION, '1' );
switch_theme( 'default' );

$result = $this->tm->update_template( 'default//does-not-exist', 'wp_template', array( 'content' => '<!-- wp:paragraph --><p>x</p><!-- /wp:paragraph -->' ) );

$this->assertInstanceOf( \WP_Error::class, $result );
$this->assertSame( 'classic_theme', $result->get_error_code() );
$data = $result->get_error_data();
$this->assertSame( 400, $data['status'] );
Comment on lines +554 to +557

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Narrow $result before calling WP_Error methods.

PHPStan still sees array|WP_Error after assertInstanceOf(), so Lines 555-556 fail static analysis.

Proposed fix
-		$this->assertInstanceOf( \WP_Error::class, $result );
+		if ( ! $result instanceof \WP_Error ) {
+			$this->fail( 'Expected a classic_theme error.' );
+			return;
+		}
 		$this->assertSame( 'classic_theme', $result->get_error_code() );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->assertInstanceOf( \WP_Error::class, $result );
$this->assertSame( 'classic_theme', $result->get_error_code() );
$data = $result->get_error_data();
$this->assertSame( 400, $data['status'] );
if ( ! $result instanceof \WP_Error ) {
$this->fail( 'Expected a classic_theme error.' );
return;
}
$this->assertSame( 'classic_theme', $result->get_error_code() );
$data = $result->get_error_data();
$this->assertSame( 400, $data['status'] );
🧰 Tools
🪛 PHPStan (2.2.5)

[error] 555-555: Cannot call method get_error_code() on array|WP_Error.

(method.nonObject)


[error] 556-556: Cannot call method get_error_data() on array|WP_Error.

(method.nonObject)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php`
around lines 554 - 557, In the test around the result assertions, explicitly
narrow or guard $result as a WP_Error after assertInstanceOf before calling
get_error_code() and get_error_data(). Preserve the existing assertions and 400
status validation while making the type refinement visible to PHPStan.

Source: Linters/SAST tools

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- wp:paragraph -->
<p>Hybrid Footer Part</p>
<!-- /wp:paragraph -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
Theme Name: Hybrid Theme
Theme URI: https://wordpress.org/
Description: For testing purposes only — a "hybrid" theme with no templates/index.html or block-templates/index.html (so wp_is_block_theme() is false) but real files under templates/ and parts/ (so get_block_templates() still finds them).
Version: 1.0.0
Text Domain: hybrid-theme
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- wp:paragraph -->
<p>Hybrid Single Template</p>
<!-- /wp:paragraph -->
Loading